@acorex-platform/module-report-management-client 0.0.15
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/fesm2022/acorex-platform-module-report-management-client-contracts.mjs +221 -0
- package/fesm2022/acorex-platform-module-report-management-client-contracts.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-report-management-client-report-drill-popup.component-CvfwhlEp.mjs +59 -0
- package/fesm2022/acorex-platform-module-report-management-client-report-drill-popup.component-CvfwhlEp.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-report-management-client-report-runner-root-page.component-DgvoIHsK.mjs +504 -0
- package/fesm2022/acorex-platform-module-report-management-client-report-runner-root-page.component-DgvoIHsK.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-report-management-client.mjs +3586 -0
- package/fesm2022/acorex-platform-module-report-management-client.mjs.map +1 -0
- package/package.json +28 -0
- package/types/acorex-platform-module-report-management-client-contracts.d.ts +554 -0
- package/types/acorex-platform-module-report-management-client.d.ts +849 -0
|
@@ -0,0 +1,3586 @@
|
|
|
1
|
+
import { AXP_MODULE_MANIFEST_PROVIDER, AXP_FEATURE_DEFINITION_PROVIDER, AXPExpressionEvaluatorService, applySystemActionDefault, AXPDeviceService } from '@acorex-platform/framework-client/core';
|
|
2
|
+
import { AXP_ENTITY_DEFINITION_LOADER, provideEntity, AXPEntityService } from '@acorex-platform/framework-client/layout/entity';
|
|
3
|
+
import { RootConfig, AXMPermissionsKeys, AXPReportManagementMenuKeys, AXPReportManagementFeatureKeys } from '@acorex-platform/module-report-management-client/contracts';
|
|
4
|
+
export * from '@acorex-platform/module-report-management-client/contracts';
|
|
5
|
+
import { AXPDomainModule } from '@acorex-platform/framework-client/domain';
|
|
6
|
+
import { AXPRootLayoutComponent } from '@acorex-platform/framework-client/themes/default';
|
|
7
|
+
import * as i0 from '@angular/core';
|
|
8
|
+
import { Injectable, inject, NgModule, computed, effect, signal, ChangeDetectionStrategy, Component, ViewEncapsulation, viewChild, untracked, DestroyRef, input } from '@angular/core';
|
|
9
|
+
import { ROUTES, ActivatedRoute } from '@angular/router';
|
|
10
|
+
import { AXP_PROTECTED_ROUTE_GUARDS, AXP_MENU_PROVIDER, AXPSettingsService, AXPCommonSettings } from '@acorex-platform/framework-client/common';
|
|
11
|
+
import { AXPSessionService, AXP_PERMISSION_DEFINITION_PROVIDER } from '@acorex-platform/framework-client/auth';
|
|
12
|
+
import { AXPRuntimeModule, provideCommandSetups, AXPCommandService } from '@acorex-platform/framework-client/runtime';
|
|
13
|
+
import * as i1 from '@acorex/core/translation';
|
|
14
|
+
import { AXTranslationService, AXTranslationModule } from '@acorex/core/translation';
|
|
15
|
+
import { AXPopupService } from '@acorex/components/popup';
|
|
16
|
+
import { get, isString } from 'lodash-es';
|
|
17
|
+
import { AXBadgeModule } from '@acorex/components/badge';
|
|
18
|
+
import * as i2$1 from '@acorex/components/button';
|
|
19
|
+
import { AXButtonModule } from '@acorex/components/button';
|
|
20
|
+
import * as i3 from '@acorex/components/decorators';
|
|
21
|
+
import { AXDecoratorModule } from '@acorex/components/decorators';
|
|
22
|
+
import { AXLoadingModule } from '@acorex/components/loading';
|
|
23
|
+
import { AXLoadingDialogService, AXLoadingDialogModule } from '@acorex/components/loading-dialog';
|
|
24
|
+
import { AXUnsubscriber } from '@acorex/core/utils';
|
|
25
|
+
import { AXPLayoutRendererComponent } from '@acorex-platform/framework-client/layout/builder';
|
|
26
|
+
import { AXPStateMessageComponent, AXPTextCommandColumnComponent, AXPThemeLayoutStartSideComponent, AXPThemeLayoutHeaderComponent, AXPThemeLayoutBlockComponent, AXPThemeLayoutFooterComponent } from '@acorex-platform/framework-client/layout/components';
|
|
27
|
+
import { AXPPageLayoutBaseComponent, AXPPageLayoutComponent, AXPPageLayoutBase } from '@acorex-platform/framework-client/layout/views';
|
|
28
|
+
import { AXPLayoutThemeService } from '@acorex-platform/framework-client/themes/shared';
|
|
29
|
+
import * as i1$1 from '@angular/common';
|
|
30
|
+
import { CommonModule } from '@angular/common';
|
|
31
|
+
import { AXPPageStatus } from '@acorex-platform/framework-shared/widget-core';
|
|
32
|
+
import { AXLocaleService } from '@acorex/core/locale';
|
|
33
|
+
import * as i2 from '@acorex-platform/framework-client/layout/widget-core';
|
|
34
|
+
import { AXPWidgetRegistryService, AXPWidgetCoreModule } from '@acorex-platform/framework-client/layout/widget-core';
|
|
35
|
+
import { Subscription } from 'rxjs';
|
|
36
|
+
import { signalStore, withState, withComputed, withMethods, patchState, withHooks } from '@ngrx/signals';
|
|
37
|
+
import { AXPPlatformScope, AXPWidgetsList, getSystemActions } from '@acorex-platform/framework-shared/core';
|
|
38
|
+
import { set } from 'lodash';
|
|
39
|
+
import { AXBarChartComponent } from '@acorex/charts/bar-chart';
|
|
40
|
+
import { AXChartLegendComponent } from '@acorex/charts/chart-legend';
|
|
41
|
+
import { AXDonutChartComponent } from '@acorex/charts/donut-chart';
|
|
42
|
+
import { AXLineChartComponent } from '@acorex/charts/line-chart';
|
|
43
|
+
import { DomSanitizer } from '@angular/platform-browser';
|
|
44
|
+
import * as i1$2 from '@acorex/components/data-table';
|
|
45
|
+
import { AXDataTableModule } from '@acorex/components/data-table';
|
|
46
|
+
import { moveItemInArray } from '@angular/cdk/drag-drop';
|
|
47
|
+
import { AXDataSource } from '@acorex/cdk/common';
|
|
48
|
+
|
|
49
|
+
//#region ---- Imports ----
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region ---- Manifest Definition ----
|
|
52
|
+
/**
|
|
53
|
+
* Report Management Module Manifest.
|
|
54
|
+
* Defines module metadata, features, and provider references.
|
|
55
|
+
*/
|
|
56
|
+
const ReportManagementManifest = {
|
|
57
|
+
name: RootConfig.module.name,
|
|
58
|
+
version: '1.0.0',
|
|
59
|
+
title: RootConfig.module.title,
|
|
60
|
+
icon: RootConfig.module.icon,
|
|
61
|
+
i18n: RootConfig.config.i18n,
|
|
62
|
+
required: true,
|
|
63
|
+
// Module dependencies (optional)
|
|
64
|
+
};
|
|
65
|
+
//#endregion
|
|
66
|
+
|
|
67
|
+
class AXMPermissionDefinitionProvider {
|
|
68
|
+
constructor(injector) {
|
|
69
|
+
this.injector = injector;
|
|
70
|
+
this.translateService = this.injector.get(AXTranslationService);
|
|
71
|
+
}
|
|
72
|
+
async define(context) {
|
|
73
|
+
const keys = AXMPermissionsKeys.ReportManagement;
|
|
74
|
+
const trans = async (key) => await this.translateService.translateAsync(key);
|
|
75
|
+
context
|
|
76
|
+
.addGroup(RootConfig.module.name, await trans('@report-management:module.menus.root.title'))
|
|
77
|
+
// Reports Management
|
|
78
|
+
.addPermission(keys.Report.Management, await trans('@report-management:reports.permissions.reports.title'))
|
|
79
|
+
.addChild(keys.Report.View, await trans('@report-management:reports.permissions.reports.actions.view.title'))
|
|
80
|
+
.addChild(keys.Report.Create, await trans('@report-management:reports.permissions.reports.actions.create.title'))
|
|
81
|
+
.addChild(keys.Report.Update, await trans('@report-management:reports.permissions.reports.actions.update.title'))
|
|
82
|
+
.addChild(keys.Report.Delete, await trans('@report-management:reports.permissions.reports.actions.delete.title'))
|
|
83
|
+
.endPermission()
|
|
84
|
+
// Categories Management
|
|
85
|
+
.addPermission(keys.Category.Management, await trans('@report-management:reports.permissions.categories.title'))
|
|
86
|
+
.addChild(keys.Category.View, await trans('@report-management:reports.permissions.categories.actions.view.title'))
|
|
87
|
+
.addChild(keys.Category.Create, await trans('@report-management:reports.permissions.categories.actions.create.title'))
|
|
88
|
+
.addChild(keys.Category.Update, await trans('@report-management:reports.permissions.categories.actions.update.title'))
|
|
89
|
+
.addChild(keys.Category.Delete, await trans('@report-management:reports.permissions.categories.actions.delete.title'))
|
|
90
|
+
.endPermission()
|
|
91
|
+
.endGroup();
|
|
92
|
+
}
|
|
93
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMPermissionDefinitionProvider, deps: [{ token: i0.Injector }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
94
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMPermissionDefinitionProvider }); }
|
|
95
|
+
}
|
|
96
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMPermissionDefinitionProvider, decorators: [{
|
|
97
|
+
type: Injectable
|
|
98
|
+
}], ctorParameters: () => [{ type: i0.Injector }] });
|
|
99
|
+
|
|
100
|
+
class AXMMenuProvider {
|
|
101
|
+
constructor() {
|
|
102
|
+
this.sessionService = inject(AXPSessionService);
|
|
103
|
+
}
|
|
104
|
+
async provide(context) {
|
|
105
|
+
const module = RootConfig.module;
|
|
106
|
+
context.addItems([
|
|
107
|
+
{
|
|
108
|
+
name: AXPReportManagementMenuKeys.Reports,
|
|
109
|
+
text: RootConfig.entities.report.titlePlural,
|
|
110
|
+
path: `${this.sessionService.application?.name}/${module.route}/reports`,
|
|
111
|
+
icon: RootConfig.entities.report.icon,
|
|
112
|
+
priority: 8300,
|
|
113
|
+
policy: {
|
|
114
|
+
features: [module.name],
|
|
115
|
+
permissions: [AXMPermissionsKeys.ReportManagement.Report.View],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class AXMEntityProvider {
|
|
123
|
+
async get(moduleName, entityName) {
|
|
124
|
+
// Simulate report entity definition
|
|
125
|
+
if (moduleName == RootConfig.module.name && entityName == RootConfig.entities.report.name) {
|
|
126
|
+
return {
|
|
127
|
+
name: "Report",
|
|
128
|
+
module: "ReportManagement",
|
|
129
|
+
title: "Report Management",
|
|
130
|
+
formats: {
|
|
131
|
+
individual: "Report Management",
|
|
132
|
+
plural: "Report Management",
|
|
133
|
+
},
|
|
134
|
+
properties: [],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
async list() {
|
|
140
|
+
const m = RootConfig.module.name;
|
|
141
|
+
return Promise.resolve(Object.values(RootConfig.entities).map((e) => ({ name: e.name, module: m })));
|
|
142
|
+
}
|
|
143
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMEntityProvider, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
144
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMEntityProvider }); }
|
|
145
|
+
}
|
|
146
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMEntityProvider, decorators: [{
|
|
147
|
+
type: Injectable
|
|
148
|
+
}] });
|
|
149
|
+
|
|
150
|
+
//#region ---- Imports ----
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region ---- Feature Definition Provider ----
|
|
153
|
+
/**
|
|
154
|
+
* Report Management Feature Definition Provider.
|
|
155
|
+
* Provides feature definitions for the Report Management module.
|
|
156
|
+
*/
|
|
157
|
+
class AXPReportManagementFeatureDefinitionProvider {
|
|
158
|
+
async provide(context) {
|
|
159
|
+
// Report Builder feature
|
|
160
|
+
context.addFeature(AXPReportManagementFeatureKeys.ReportBuilder, {
|
|
161
|
+
title: '@report-management:features.report-builder.title',
|
|
162
|
+
description: '@report-management:features.report-builder.description',
|
|
163
|
+
defaultValue: true,
|
|
164
|
+
type: 'boolean',
|
|
165
|
+
});
|
|
166
|
+
// Saved Reports feature
|
|
167
|
+
context.addFeature(AXPReportManagementFeatureKeys.SavedReports, {
|
|
168
|
+
title: '@report-management:features.saved-reports.title',
|
|
169
|
+
description: '@report-management:features.saved-reports.description',
|
|
170
|
+
defaultValue: true,
|
|
171
|
+
type: 'boolean',
|
|
172
|
+
});
|
|
173
|
+
// Scheduled Reports feature
|
|
174
|
+
context.addFeature(AXPReportManagementFeatureKeys.ScheduledReports, {
|
|
175
|
+
title: '@report-management:features.scheduled-reports.title',
|
|
176
|
+
description: '@report-management:features.scheduled-reports.description',
|
|
177
|
+
defaultValue: true,
|
|
178
|
+
type: 'boolean',
|
|
179
|
+
});
|
|
180
|
+
// Background Reports feature
|
|
181
|
+
context.addFeature(AXPReportManagementFeatureKeys.BackgroundReports, {
|
|
182
|
+
title: '@report-management:features.background-reports.title',
|
|
183
|
+
description: '@report-management:features.background-reports.description',
|
|
184
|
+
defaultValue: true,
|
|
185
|
+
type: 'boolean',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
|
|
191
|
+
function routesFacory() {
|
|
192
|
+
let routes = [
|
|
193
|
+
{
|
|
194
|
+
path: `:app/${RootConfig.module.route}/viewer`,
|
|
195
|
+
canActivate: [...AXP_PROTECTED_ROUTE_GUARDS],
|
|
196
|
+
data: { tab: true, rootLayout: true },
|
|
197
|
+
loadComponent: () => {
|
|
198
|
+
return AXPRootLayoutComponent;
|
|
199
|
+
},
|
|
200
|
+
children: [
|
|
201
|
+
{
|
|
202
|
+
path: ':reportId',
|
|
203
|
+
loadComponent: () => Promise.resolve().then(function () { return reportViewerPage_component; }).then((c) => c.AXPReportViewerPageComponent),
|
|
204
|
+
data: { reuse: true },
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
path: '',
|
|
208
|
+
loadComponent: () => Promise.resolve().then(function () { return reportViewerPage_component; }).then((c) => c.AXPReportViewerPageComponent),
|
|
209
|
+
data: { reuse: true },
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
path: `:app/${RootConfig.module.route}/reports`,
|
|
215
|
+
canActivate: [...AXP_PROTECTED_ROUTE_GUARDS],
|
|
216
|
+
data: { tab: true, rootLayout: true },
|
|
217
|
+
loadComponent: () => {
|
|
218
|
+
return AXPRootLayoutComponent;
|
|
219
|
+
},
|
|
220
|
+
children: [
|
|
221
|
+
{
|
|
222
|
+
path: '**',
|
|
223
|
+
loadComponent: () => import('./acorex-platform-module-report-management-client-report-runner-root-page.component-DgvoIHsK.mjs').then((c) => c.AXMReportRunnerRootPageComponent),
|
|
224
|
+
data: { reuse: true },
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
return routes;
|
|
230
|
+
}
|
|
231
|
+
class AXMReportManagementModule {
|
|
232
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMReportManagementModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
233
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: AXMReportManagementModule, imports: [AXPDomainModule,
|
|
234
|
+
AXPRuntimeModule] }); }
|
|
235
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMReportManagementModule, providers: [
|
|
236
|
+
{
|
|
237
|
+
provide: ROUTES,
|
|
238
|
+
multi: true,
|
|
239
|
+
useFactory: routesFacory,
|
|
240
|
+
},
|
|
241
|
+
provideEntity([RootConfig.entities.report.fullName, RootConfig.entities.category.fullName]),
|
|
242
|
+
// Module Manifest Provider
|
|
243
|
+
{
|
|
244
|
+
provide: AXP_MODULE_MANIFEST_PROVIDER,
|
|
245
|
+
useValue: ReportManagementManifest,
|
|
246
|
+
multi: true,
|
|
247
|
+
},
|
|
248
|
+
// Provider references (now using tokens)
|
|
249
|
+
{
|
|
250
|
+
provide: AXP_PERMISSION_DEFINITION_PROVIDER,
|
|
251
|
+
useClass: AXMPermissionDefinitionProvider,
|
|
252
|
+
multi: true,
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
provide: AXP_MENU_PROVIDER,
|
|
256
|
+
useClass: AXMMenuProvider,
|
|
257
|
+
multi: true,
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
provide: AXP_ENTITY_DEFINITION_LOADER,
|
|
261
|
+
useClass: AXMEntityProvider,
|
|
262
|
+
multi: true,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
provide: AXP_FEATURE_DEFINITION_PROVIDER,
|
|
266
|
+
useClass: AXPReportManagementFeatureDefinitionProvider,
|
|
267
|
+
multi: true,
|
|
268
|
+
},
|
|
269
|
+
provideCommandSetups([
|
|
270
|
+
{
|
|
271
|
+
key: 'ReportManagement.Report:DrillToReport',
|
|
272
|
+
command: () => Promise.resolve().then(function () { return drillToReport_command; }).then((c) => c.AXPDrillToReportCommand),
|
|
273
|
+
},
|
|
274
|
+
]),
|
|
275
|
+
], imports: [AXPDomainModule,
|
|
276
|
+
AXPRuntimeModule] }); }
|
|
277
|
+
}
|
|
278
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMReportManagementModule, decorators: [{
|
|
279
|
+
type: NgModule,
|
|
280
|
+
args: [{
|
|
281
|
+
imports: [
|
|
282
|
+
AXPDomainModule,
|
|
283
|
+
AXPRuntimeModule,
|
|
284
|
+
// Entity Modules
|
|
285
|
+
],
|
|
286
|
+
exports: [],
|
|
287
|
+
declarations: [],
|
|
288
|
+
providers: [
|
|
289
|
+
{
|
|
290
|
+
provide: ROUTES,
|
|
291
|
+
multi: true,
|
|
292
|
+
useFactory: routesFacory,
|
|
293
|
+
},
|
|
294
|
+
provideEntity([RootConfig.entities.report.fullName, RootConfig.entities.category.fullName]),
|
|
295
|
+
// Module Manifest Provider
|
|
296
|
+
{
|
|
297
|
+
provide: AXP_MODULE_MANIFEST_PROVIDER,
|
|
298
|
+
useValue: ReportManagementManifest,
|
|
299
|
+
multi: true,
|
|
300
|
+
},
|
|
301
|
+
// Provider references (now using tokens)
|
|
302
|
+
{
|
|
303
|
+
provide: AXP_PERMISSION_DEFINITION_PROVIDER,
|
|
304
|
+
useClass: AXMPermissionDefinitionProvider,
|
|
305
|
+
multi: true,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
provide: AXP_MENU_PROVIDER,
|
|
309
|
+
useClass: AXMMenuProvider,
|
|
310
|
+
multi: true,
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
provide: AXP_ENTITY_DEFINITION_LOADER,
|
|
314
|
+
useClass: AXMEntityProvider,
|
|
315
|
+
multi: true,
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
provide: AXP_FEATURE_DEFINITION_PROVIDER,
|
|
319
|
+
useClass: AXPReportManagementFeatureDefinitionProvider,
|
|
320
|
+
multi: true,
|
|
321
|
+
},
|
|
322
|
+
provideCommandSetups([
|
|
323
|
+
{
|
|
324
|
+
key: 'ReportManagement.Report:DrillToReport',
|
|
325
|
+
command: () => Promise.resolve().then(function () { return drillToReport_command; }).then((c) => c.AXPDrillToReportCommand),
|
|
326
|
+
},
|
|
327
|
+
]),
|
|
328
|
+
],
|
|
329
|
+
}]
|
|
330
|
+
}] });
|
|
331
|
+
|
|
332
|
+
//#endregion
|
|
333
|
+
class AXPDrillToReportCommand {
|
|
334
|
+
constructor() {
|
|
335
|
+
//#region ---- Services & Dependencies ----
|
|
336
|
+
this.popupService = inject(AXPopupService);
|
|
337
|
+
this.expressionEvaluator = inject(AXPExpressionEvaluatorService);
|
|
338
|
+
this.translate = inject(AXTranslationService);
|
|
339
|
+
}
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region ---- Command Implementation ----
|
|
342
|
+
async execute(input) {
|
|
343
|
+
const rowData = this.resolveRowData(input);
|
|
344
|
+
const options = await this.resolveOptions(input, rowData);
|
|
345
|
+
if (!options.reportId) {
|
|
346
|
+
return {
|
|
347
|
+
success: false,
|
|
348
|
+
message: {
|
|
349
|
+
text: await this.translate.translateAsync('@report-management:report-viewer.messages.error.invalid-drill-target'),
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const popupTitle = await this.resolvePopupTitle(options.title, rowData);
|
|
354
|
+
const comp = (await import('./acorex-platform-module-report-management-client-report-drill-popup.component-CvfwhlEp.mjs')).AXPReportDrillPopupComponent;
|
|
355
|
+
this.popupService.open(comp, {
|
|
356
|
+
title: popupTitle,
|
|
357
|
+
size: 'lg',
|
|
358
|
+
data: {
|
|
359
|
+
reportId: options.reportId,
|
|
360
|
+
layoutId: options.layoutId,
|
|
361
|
+
parameters: options.parameters ?? {},
|
|
362
|
+
hideColumnFields: options.hideColumnFields ?? [],
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
return { success: true };
|
|
366
|
+
}
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region ---- Helpers ----
|
|
369
|
+
resolveRowData(input) {
|
|
370
|
+
const ctx = input.__context__;
|
|
371
|
+
if (ctx?.data && typeof ctx.data === 'object') {
|
|
372
|
+
return ctx.data;
|
|
373
|
+
}
|
|
374
|
+
if (input['data'] && typeof input['data'] === 'object') {
|
|
375
|
+
return input['data'];
|
|
376
|
+
}
|
|
377
|
+
return input;
|
|
378
|
+
}
|
|
379
|
+
async resolveOptions(input, rowData) {
|
|
380
|
+
const ctx = input.__context__;
|
|
381
|
+
const typedInput = input;
|
|
382
|
+
const rawOptions = {
|
|
383
|
+
reportId: typedInput.reportId,
|
|
384
|
+
layoutId: typedInput.layoutId,
|
|
385
|
+
title: typedInput.title,
|
|
386
|
+
parameters: typedInput.parameters,
|
|
387
|
+
hideColumnFields: typedInput.hideColumnFields,
|
|
388
|
+
...(typedInput.options ?? {}),
|
|
389
|
+
...(ctx?.options ?? {}),
|
|
390
|
+
};
|
|
391
|
+
const scope = {
|
|
392
|
+
context: {
|
|
393
|
+
eval: (path) => get(rowData, path),
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
return (await this.expressionEvaluator.evaluate(rawOptions, scope));
|
|
397
|
+
}
|
|
398
|
+
async resolvePopupTitle(title, rowData) {
|
|
399
|
+
if (!isString(title) || !title.trim()) {
|
|
400
|
+
return await this.translate.translateAsync('@report-management:report-viewer.components.report-drill-popup.title');
|
|
401
|
+
}
|
|
402
|
+
const scope = {
|
|
403
|
+
context: {
|
|
404
|
+
eval: (path) => get(rowData, path),
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
const evaluated = await this.expressionEvaluator.evaluate(title, scope);
|
|
408
|
+
return isString(evaluated) ? evaluated : title;
|
|
409
|
+
}
|
|
410
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPDrillToReportCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
411
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPDrillToReportCommand, providedIn: 'root' }); }
|
|
412
|
+
}
|
|
413
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPDrillToReportCommand, decorators: [{
|
|
414
|
+
type: Injectable,
|
|
415
|
+
args: [{ providedIn: 'root' }]
|
|
416
|
+
}] });
|
|
417
|
+
|
|
418
|
+
var drillToReport_command = /*#__PURE__*/Object.freeze({
|
|
419
|
+
__proto__: null,
|
|
420
|
+
AXPDrillToReportCommand: AXPDrillToReportCommand
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
//#endregion
|
|
424
|
+
|
|
425
|
+
//#region ---- Imports ----
|
|
426
|
+
//#endregion
|
|
427
|
+
/**
|
|
428
|
+
* Report Viewer Service
|
|
429
|
+
*
|
|
430
|
+
* Handles report definition retrieval, caching, and layout actions.
|
|
431
|
+
*
|
|
432
|
+
* Performance optimizations:
|
|
433
|
+
* 1. Uses childrenCount to determine if category has children (no query needed)
|
|
434
|
+
* 2. Uses itemsCount to determine if category has reports (no query needed)
|
|
435
|
+
* 3. Aggressive caching prevents duplicate API calls
|
|
436
|
+
* 4. Single pending request per resource prevents race conditions
|
|
437
|
+
* 5. Lazy loading - only loads data when needed
|
|
438
|
+
*/
|
|
439
|
+
class AXPReportViewerService {
|
|
440
|
+
constructor() {
|
|
441
|
+
//#region ---- Dependencies ----
|
|
442
|
+
this.entityService = inject(AXPEntityService);
|
|
443
|
+
this.reportCategoryService = this.entityService.withEntity(RootConfig.module.name, RootConfig.entities.report.name + "Category").data();
|
|
444
|
+
this.reportDefinitionService = this.entityService.withEntity(RootConfig.module.name, RootConfig.entities.report.name).data();
|
|
445
|
+
//#endregion
|
|
446
|
+
//#region ---- Cache Storage ----
|
|
447
|
+
/** Cache for categories by id - O(1) lookup */
|
|
448
|
+
this.categoriesById = new Map();
|
|
449
|
+
/** Cache for categories by parentId - O(1) lookup */
|
|
450
|
+
this.categoriesByParentId = new Map();
|
|
451
|
+
/** Cache for report definitions by categoryId - O(1) lookup */
|
|
452
|
+
this.definitionsByCategory = new Map();
|
|
453
|
+
/** Cache for individual report definitions by id - O(1) lookup */
|
|
454
|
+
this.definitionsById = new Map();
|
|
455
|
+
/** Pending API requests to prevent duplicate calls */
|
|
456
|
+
this.pendingCategoriesRequests = new Map();
|
|
457
|
+
this.pendingDefinitionsRequests = new Map();
|
|
458
|
+
this.pendingReportRequests = new Map();
|
|
459
|
+
}
|
|
460
|
+
//#endregion
|
|
461
|
+
//#region ---- Public API: Categories ----
|
|
462
|
+
/**
|
|
463
|
+
* Get categories by parentId with aggressive caching
|
|
464
|
+
*
|
|
465
|
+
* Optimization: Returns cached result immediately if available,
|
|
466
|
+
* preventing unnecessary API calls during navigation
|
|
467
|
+
*
|
|
468
|
+
* @param parentId - Parent category ID (undefined = root categories)
|
|
469
|
+
* @returns Array of categories with count metadata (childrenCount, itemsCount)
|
|
470
|
+
*/
|
|
471
|
+
async getCategories(parentId) {
|
|
472
|
+
// ✅ Fast path: Return cached result
|
|
473
|
+
if (this.categoriesByParentId.has(parentId)) {
|
|
474
|
+
return this.categoriesByParentId.get(parentId);
|
|
475
|
+
}
|
|
476
|
+
// ✅ Prevent duplicate requests: Return pending promise
|
|
477
|
+
if (this.pendingCategoriesRequests.has(parentId)) {
|
|
478
|
+
return this.pendingCategoriesRequests.get(parentId);
|
|
479
|
+
}
|
|
480
|
+
// ✅ Create single request and cache it
|
|
481
|
+
const requestPromise = this.loadCategories(parentId);
|
|
482
|
+
this.pendingCategoriesRequests.set(parentId, requestPromise);
|
|
483
|
+
return requestPromise;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Get single category by ID with O(1) lookup
|
|
487
|
+
*
|
|
488
|
+
* Optimization: Uses Map for instant retrieval, falls back to
|
|
489
|
+
* searching cache, then providers if not found
|
|
490
|
+
*/
|
|
491
|
+
async getCategoryById(categoryId) {
|
|
492
|
+
// ✅ Fast path: O(1) lookup in cache
|
|
493
|
+
if (this.categoriesById.has(categoryId)) {
|
|
494
|
+
return this.categoriesById.get(categoryId);
|
|
495
|
+
}
|
|
496
|
+
// ✅ Search in cached parent-child lists
|
|
497
|
+
for (const categories of this.categoriesByParentId.values()) {
|
|
498
|
+
const found = categories.find(cat => cat.id === categoryId);
|
|
499
|
+
if (found) {
|
|
500
|
+
this.categoriesById.set(categoryId, found);
|
|
501
|
+
return found;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// ✅ Load root categories if not loaded
|
|
505
|
+
if (!this.categoriesByParentId.has(undefined)) {
|
|
506
|
+
await this.getCategories();
|
|
507
|
+
if (this.categoriesById.has(categoryId)) {
|
|
508
|
+
return this.categoriesById.get(categoryId);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// ✅ Breadth-first search through hierarchy
|
|
512
|
+
return this.searchCategoryInHierarchy(categoryId);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Get category path from root to specified category
|
|
516
|
+
*
|
|
517
|
+
* Optimization: Builds path using cached categories only
|
|
518
|
+
*/
|
|
519
|
+
async getCategoriesPathById(categoryId) {
|
|
520
|
+
const path = [];
|
|
521
|
+
let currentCategoryId = categoryId;
|
|
522
|
+
while (currentCategoryId) {
|
|
523
|
+
const category = await this.getCategoryById(currentCategoryId);
|
|
524
|
+
if (!category) {
|
|
525
|
+
throw new Error(`Category '${currentCategoryId}' not found`);
|
|
526
|
+
}
|
|
527
|
+
path.unshift(category);
|
|
528
|
+
currentCategoryId = category.parentId;
|
|
529
|
+
}
|
|
530
|
+
return path;
|
|
531
|
+
}
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region ---- Public API: Report Definitions ----
|
|
534
|
+
/**
|
|
535
|
+
* Get report definitions for a category with smart caching
|
|
536
|
+
*
|
|
537
|
+
* Optimization: Checks itemsCount before querying
|
|
538
|
+
* - If itemsCount = 0, returns empty array (no API call)
|
|
539
|
+
* - If itemsCount > 0, loads and caches reports
|
|
540
|
+
* - Returns cached result on subsequent calls
|
|
541
|
+
*
|
|
542
|
+
* @param categoryId - Category ID to get reports from
|
|
543
|
+
* @returns Array of report definitions
|
|
544
|
+
*/
|
|
545
|
+
async getReportsByCategoryId(categoryId) {
|
|
546
|
+
// ✅ Fast path: Return cached result
|
|
547
|
+
if (this.definitionsByCategory.has(categoryId)) {
|
|
548
|
+
return this.definitionsByCategory.get(categoryId);
|
|
549
|
+
}
|
|
550
|
+
// ✅ Smart optimization: Check itemsCount before querying
|
|
551
|
+
const category = await this.getCategoryById(categoryId);
|
|
552
|
+
if (category && category.itemsCount !== undefined && category.itemsCount === 0) {
|
|
553
|
+
// Category has no reports - cache empty array and skip API call
|
|
554
|
+
const emptyArray = [];
|
|
555
|
+
this.definitionsByCategory.set(categoryId, emptyArray);
|
|
556
|
+
return emptyArray;
|
|
557
|
+
}
|
|
558
|
+
// ✅ Prevent duplicate requests
|
|
559
|
+
if (this.pendingDefinitionsRequests.has(categoryId)) {
|
|
560
|
+
return this.pendingDefinitionsRequests.get(categoryId);
|
|
561
|
+
}
|
|
562
|
+
// ✅ Load from entity service
|
|
563
|
+
const requestPromise = this.loadReports(categoryId);
|
|
564
|
+
this.pendingDefinitionsRequests.set(categoryId, requestPromise);
|
|
565
|
+
return requestPromise;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Get single report definition by ID with O(1) lookup
|
|
569
|
+
*
|
|
570
|
+
* Optimization: Uses Map for instant retrieval
|
|
571
|
+
*/
|
|
572
|
+
async getReportById(id) {
|
|
573
|
+
// ✅ Fast path: O(1) lookup in cache
|
|
574
|
+
if (this.definitionsById.has(id)) {
|
|
575
|
+
return this.definitionsById.get(id);
|
|
576
|
+
}
|
|
577
|
+
// ✅ Prevent duplicate requests
|
|
578
|
+
if (this.pendingReportRequests.has(id)) {
|
|
579
|
+
return this.pendingReportRequests.get(id);
|
|
580
|
+
}
|
|
581
|
+
// ✅ Load from entity service
|
|
582
|
+
const requestPromise = this.loadReport(id);
|
|
583
|
+
this.pendingReportRequests.set(id, requestPromise);
|
|
584
|
+
return requestPromise;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Get category ID containing a specific report
|
|
588
|
+
*
|
|
589
|
+
* Optimization: Searches cache first, loads on-demand if needed
|
|
590
|
+
*/
|
|
591
|
+
async getCategoryIdByReportId(reportId) {
|
|
592
|
+
// ✅ Search in cached reports
|
|
593
|
+
for (const [categoryId, reports] of this.definitionsByCategory.entries()) {
|
|
594
|
+
if (reports.some(report => report.id === reportId)) {
|
|
595
|
+
return categoryId;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// ✅ Try loading the report to find its category
|
|
599
|
+
const report = await this.getReportById(reportId);
|
|
600
|
+
if (report && report.categoryIds && report.categoryIds.length > 0) {
|
|
601
|
+
return report.categoryIds[0]; // Return first category
|
|
602
|
+
}
|
|
603
|
+
return undefined;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Get category path for a report
|
|
607
|
+
*/
|
|
608
|
+
async getCategoriesPathByReportId(reportId) {
|
|
609
|
+
const categoryId = await this.getCategoryIdByReportId(reportId);
|
|
610
|
+
if (!categoryId) {
|
|
611
|
+
throw new Error(`Report '${reportId}' not found in any category`);
|
|
612
|
+
}
|
|
613
|
+
return this.getCategoriesPathById(categoryId);
|
|
614
|
+
}
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region ---- Public API: Layout Actions ----
|
|
617
|
+
/**
|
|
618
|
+
* Get general actions list for a layout type
|
|
619
|
+
* Override in connectivity layer (mock/API) to provide general actions
|
|
620
|
+
* Base implementation returns empty array
|
|
621
|
+
*
|
|
622
|
+
* @param layoutType - The layout type
|
|
623
|
+
* @returns Array of general actions for the layout type
|
|
624
|
+
*/
|
|
625
|
+
getGeneralActionsList(layoutType) {
|
|
626
|
+
return [];
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Get merged actions for a layout definition
|
|
630
|
+
* Combines general actions (from getGeneralActionsList) with layout-specific actions
|
|
631
|
+
* Layout-specific actions take precedence (by name) over general actions
|
|
632
|
+
*
|
|
633
|
+
* @param layoutDefinition - The layout definition
|
|
634
|
+
* @returns Merged array of actions (general + custom)
|
|
635
|
+
*/
|
|
636
|
+
getMergedActions(layoutDefinition) {
|
|
637
|
+
const generalActions = this.getGeneralActionsList(layoutDefinition.layout.type);
|
|
638
|
+
const customActions = layoutDefinition.actions || [];
|
|
639
|
+
// Create a map of custom actions by name for quick lookup
|
|
640
|
+
const customActionsMap = new Map();
|
|
641
|
+
customActions.forEach(action => {
|
|
642
|
+
if (action.name) {
|
|
643
|
+
customActionsMap.set(action.name, action);
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
// Merge: Start with general actions, replace with custom if name matches
|
|
647
|
+
const merged = [];
|
|
648
|
+
const usedNames = new Set();
|
|
649
|
+
// Add general actions (unless overridden by custom)
|
|
650
|
+
generalActions.forEach(action => {
|
|
651
|
+
if (action.name && customActionsMap.has(action.name)) {
|
|
652
|
+
// Custom action overrides general action
|
|
653
|
+
merged.push(customActionsMap.get(action.name));
|
|
654
|
+
usedNames.add(action.name);
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
merged.push(action);
|
|
658
|
+
if (action.name) {
|
|
659
|
+
usedNames.add(action.name);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
// Add custom actions that weren't already added (new actions)
|
|
664
|
+
customActions.forEach(action => {
|
|
665
|
+
if (action.name && !usedNames.has(action.name)) {
|
|
666
|
+
merged.push(action);
|
|
667
|
+
usedNames.add(action.name);
|
|
668
|
+
}
|
|
669
|
+
else if (!action.name) {
|
|
670
|
+
// Actions without names are always added
|
|
671
|
+
merged.push(action);
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
return merged;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Get primary actions for a layout definition
|
|
678
|
+
*/
|
|
679
|
+
getPrimaryActions(layoutDefinition) {
|
|
680
|
+
return this.getMergedActions(layoutDefinition).filter(action => action.priority === 'primary');
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Get secondary actions for a layout definition
|
|
684
|
+
*/
|
|
685
|
+
getSecondaryActions(layoutDefinition) {
|
|
686
|
+
return this.getMergedActions(layoutDefinition).filter(action => action.priority === 'secondary');
|
|
687
|
+
}
|
|
688
|
+
//#endregion
|
|
689
|
+
//#region ---- Private: Data Loading ----
|
|
690
|
+
/**
|
|
691
|
+
* Load categories from entity service and cache results
|
|
692
|
+
* Can be overridden in extended services (e.g., API layer)
|
|
693
|
+
*/
|
|
694
|
+
async loadCategories(parentId) {
|
|
695
|
+
try {
|
|
696
|
+
const list = [];
|
|
697
|
+
if (parentId) {
|
|
698
|
+
const result = await this.reportCategoryService.query({
|
|
699
|
+
skip: 0,
|
|
700
|
+
take: 1000,
|
|
701
|
+
filter: {
|
|
702
|
+
logic: 'and',
|
|
703
|
+
filters: [{
|
|
704
|
+
field: 'parentId',
|
|
705
|
+
operator: {
|
|
706
|
+
type: 'equal',
|
|
707
|
+
},
|
|
708
|
+
value: parentId,
|
|
709
|
+
}]
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
list.push(...result.items);
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
const result = await this.reportCategoryService.query();
|
|
716
|
+
list.push(...result.items);
|
|
717
|
+
}
|
|
718
|
+
// ✅ Cache results for fast subsequent access
|
|
719
|
+
this.categoriesByParentId.set(parentId, list);
|
|
720
|
+
list.forEach(cat => this.categoriesById.set(cat.id, cat));
|
|
721
|
+
return list;
|
|
722
|
+
}
|
|
723
|
+
finally {
|
|
724
|
+
this.pendingCategoriesRequests.delete(parentId);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Load report definitions from entity service and cache results
|
|
729
|
+
* Can be overridden in extended services (e.g., API layer)
|
|
730
|
+
*/
|
|
731
|
+
async loadReports(categoryId) {
|
|
732
|
+
try {
|
|
733
|
+
const list = [];
|
|
734
|
+
// Use 'contains' operator to filter by categoryIds array field
|
|
735
|
+
const result = await this.reportDefinitionService.query({
|
|
736
|
+
skip: 0,
|
|
737
|
+
take: 1000,
|
|
738
|
+
filter: {
|
|
739
|
+
logic: 'and',
|
|
740
|
+
filters: [{
|
|
741
|
+
field: 'categoryIds',
|
|
742
|
+
operator: {
|
|
743
|
+
type: 'contains',
|
|
744
|
+
},
|
|
745
|
+
value: categoryId,
|
|
746
|
+
}]
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
list.push(...result.items);
|
|
750
|
+
// ✅ Cache results for fast subsequent access
|
|
751
|
+
this.definitionsByCategory.set(categoryId, list);
|
|
752
|
+
list.forEach(def => this.definitionsById.set(def.id, def));
|
|
753
|
+
return list;
|
|
754
|
+
}
|
|
755
|
+
finally {
|
|
756
|
+
this.pendingDefinitionsRequests.delete(categoryId);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Load single report from entity service and cache result
|
|
761
|
+
* Can be overridden in extended services (e.g., API layer)
|
|
762
|
+
*/
|
|
763
|
+
async loadReport(id) {
|
|
764
|
+
try {
|
|
765
|
+
const result = await this.reportDefinitionService.byKey(id);
|
|
766
|
+
if (result) {
|
|
767
|
+
this.definitionsById.set(id, result);
|
|
768
|
+
return result;
|
|
769
|
+
}
|
|
770
|
+
// Fallback: Search in cached reports
|
|
771
|
+
for (const reports of this.definitionsByCategory.values()) {
|
|
772
|
+
const found = reports.find(report => report.id === id);
|
|
773
|
+
if (found) {
|
|
774
|
+
this.definitionsById.set(id, found);
|
|
775
|
+
return found;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return undefined;
|
|
779
|
+
}
|
|
780
|
+
finally {
|
|
781
|
+
this.pendingReportRequests.delete(id);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Breadth-first search through category hierarchy
|
|
786
|
+
*/
|
|
787
|
+
async searchCategoryInHierarchy(categoryId) {
|
|
788
|
+
const searchQueue = [undefined];
|
|
789
|
+
const searched = new Set();
|
|
790
|
+
while (searchQueue.length > 0) {
|
|
791
|
+
const parentId = searchQueue.shift();
|
|
792
|
+
if (searched.has(parentId))
|
|
793
|
+
continue;
|
|
794
|
+
searched.add(parentId);
|
|
795
|
+
const categories = await this.getCategories(parentId);
|
|
796
|
+
const found = categories.find(cat => cat.id === categoryId);
|
|
797
|
+
if (found) {
|
|
798
|
+
return found;
|
|
799
|
+
}
|
|
800
|
+
// ✅ Optimization: Only search children if childrenCount > 0
|
|
801
|
+
for (const category of categories) {
|
|
802
|
+
if (category.childrenCount > 0 && !searched.has(category.id)) {
|
|
803
|
+
searchQueue.push(category.id);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return undefined;
|
|
808
|
+
}
|
|
809
|
+
//#endregion
|
|
810
|
+
//#region ---- Cache Management ----
|
|
811
|
+
/**
|
|
812
|
+
* Check if category has children (uses cached count)
|
|
813
|
+
*/
|
|
814
|
+
categoryHasChildren(categoryId) {
|
|
815
|
+
const category = this.categoriesById.get(categoryId);
|
|
816
|
+
return category ? category.childrenCount > 0 : false;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Check if category has reports (uses cached count)
|
|
820
|
+
*/
|
|
821
|
+
categoryHasReports(categoryId) {
|
|
822
|
+
const category = this.categoriesById.get(categoryId);
|
|
823
|
+
return category ? (category.itemsCount ?? 0) > 0 : false;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Protected: Check if categories are cached for a parentId
|
|
827
|
+
* Used by extended services (e.g., mock) to determine if logging is needed
|
|
828
|
+
*/
|
|
829
|
+
isCategoriesCached(parentId) {
|
|
830
|
+
return this.categoriesByParentId.has(parentId) || this.pendingCategoriesRequests.has(parentId);
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Protected: Check if a category is cached by ID
|
|
834
|
+
* Used by extended services (e.g., mock) to determine if logging is needed
|
|
835
|
+
*/
|
|
836
|
+
isCategoryCached(categoryId) {
|
|
837
|
+
if (this.categoriesById.has(categoryId)) {
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
// Check if it might be in a cached parent list
|
|
841
|
+
for (const categories of this.categoriesByParentId.values()) {
|
|
842
|
+
if (categories.some(cat => cat.id === categoryId)) {
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Protected: Check if reports are cached for a categoryId
|
|
850
|
+
* Used by extended services (e.g., mock) to determine if logging is needed
|
|
851
|
+
*/
|
|
852
|
+
isReportsCached(categoryId) {
|
|
853
|
+
return this.definitionsByCategory.has(categoryId) || this.pendingDefinitionsRequests.has(categoryId);
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Protected: Check if a report is cached by ID
|
|
857
|
+
* Used by extended services (e.g., mock) to determine if logging is needed
|
|
858
|
+
*/
|
|
859
|
+
isReportCached(reportId) {
|
|
860
|
+
return this.definitionsById.has(reportId) || this.pendingReportRequests.has(reportId);
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Clear all caches
|
|
864
|
+
*/
|
|
865
|
+
clearAllCache() {
|
|
866
|
+
this.categoriesById.clear();
|
|
867
|
+
this.categoriesByParentId.clear();
|
|
868
|
+
this.definitionsByCategory.clear();
|
|
869
|
+
this.definitionsById.clear();
|
|
870
|
+
this.pendingCategoriesRequests.clear();
|
|
871
|
+
this.pendingDefinitionsRequests.clear();
|
|
872
|
+
this.pendingReportRequests.clear();
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Clear categories cache only
|
|
876
|
+
*/
|
|
877
|
+
clearCategoriesCache() {
|
|
878
|
+
this.categoriesById.clear();
|
|
879
|
+
this.categoriesByParentId.clear();
|
|
880
|
+
this.pendingCategoriesRequests.clear();
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Clear reports cache only
|
|
884
|
+
*/
|
|
885
|
+
clearReportsCache() {
|
|
886
|
+
this.definitionsByCategory.clear();
|
|
887
|
+
this.definitionsById.clear();
|
|
888
|
+
this.pendingDefinitionsRequests.clear();
|
|
889
|
+
this.pendingReportRequests.clear();
|
|
890
|
+
}
|
|
891
|
+
//#endregion
|
|
892
|
+
//#endregion
|
|
893
|
+
//#region ---- Report Execution (Override in Connectivity Layer) ----
|
|
894
|
+
/**
|
|
895
|
+
* Execute report with given context
|
|
896
|
+
* Override in connectivity layer (mock/API) to implement report execution
|
|
897
|
+
*
|
|
898
|
+
* @param context - Layout execution context with reportId, layoutId, dataSource, and filters
|
|
899
|
+
* @returns Report execution result with layout and data
|
|
900
|
+
*/
|
|
901
|
+
async executeReport(context) {
|
|
902
|
+
throw new Error('executeReport must be implemented in connectivity layer (mock/API)');
|
|
903
|
+
}
|
|
904
|
+
//#endregion
|
|
905
|
+
//#region ---- Action Handlers (Override in Connectivity Layer) ----
|
|
906
|
+
/**
|
|
907
|
+
* Execute action by command name
|
|
908
|
+
* Override in connectivity layer (mock/API) to implement action execution
|
|
909
|
+
*
|
|
910
|
+
* @param commandName - Command name (e.g., 'download-report', 'print-report', 'export-report')
|
|
911
|
+
* @param options - Command options (e.g., { format: 'excel', pdfData: {...}, reportData: {...} })
|
|
912
|
+
*/
|
|
913
|
+
async executeAction(commandName, options) {
|
|
914
|
+
console.log(`Execute action: ${commandName} - should be implemented in connectivity layer`, options);
|
|
915
|
+
}
|
|
916
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportViewerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
917
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportViewerService, providedIn: 'root' }); }
|
|
918
|
+
}
|
|
919
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportViewerService, decorators: [{
|
|
920
|
+
type: Injectable,
|
|
921
|
+
args: [{
|
|
922
|
+
providedIn: 'root',
|
|
923
|
+
}]
|
|
924
|
+
}] });
|
|
925
|
+
|
|
926
|
+
function AXPieChartReportMapper(data) {
|
|
927
|
+
return Object.entries(data).map(([key, value]) => ({
|
|
928
|
+
id: key,
|
|
929
|
+
label: key,
|
|
930
|
+
value: value,
|
|
931
|
+
}));
|
|
932
|
+
}
|
|
933
|
+
function AXBarChartReportMapper(data) {
|
|
934
|
+
return Object.entries(data).map(([key, value]) => ({
|
|
935
|
+
id: key,
|
|
936
|
+
label: key,
|
|
937
|
+
value: value,
|
|
938
|
+
}));
|
|
939
|
+
}
|
|
940
|
+
function AXLineChartReportMapper(data) {
|
|
941
|
+
const entries = Object.entries(data);
|
|
942
|
+
if (entries.length === 0) {
|
|
943
|
+
return [];
|
|
944
|
+
}
|
|
945
|
+
const dataPoints = entries.map(([key, value]) => ({
|
|
946
|
+
x: key,
|
|
947
|
+
y: value,
|
|
948
|
+
}));
|
|
949
|
+
return [
|
|
950
|
+
{
|
|
951
|
+
id: 'series-1',
|
|
952
|
+
label: 'Data Series',
|
|
953
|
+
tooltipLabel: 'Countries Info',
|
|
954
|
+
lineColor: '#d81159',
|
|
955
|
+
fillColor: '#d8115920',
|
|
956
|
+
data: dataPoints,
|
|
957
|
+
},
|
|
958
|
+
];
|
|
959
|
+
}
|
|
960
|
+
//#region ---- Helpers ----
|
|
961
|
+
/**
|
|
962
|
+
* Determines whether a filter value should be treated as empty.
|
|
963
|
+
* This handles deep objects, arrays, and primitives so that
|
|
964
|
+
* values like { from: null, to: null } are considered empty.
|
|
965
|
+
*/
|
|
966
|
+
function isEmptyFilterValue(value) {
|
|
967
|
+
if (value === null || value === undefined || value === '') {
|
|
968
|
+
return true;
|
|
969
|
+
}
|
|
970
|
+
if (Array.isArray(value)) {
|
|
971
|
+
return value.length === 0 || value.every((item) => isEmptyFilterValue(item));
|
|
972
|
+
}
|
|
973
|
+
if (typeof value === 'object') {
|
|
974
|
+
const entries = Object.entries(value);
|
|
975
|
+
if (entries.length === 0) {
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
// Consider object empty if all nested values are empty
|
|
979
|
+
return entries.every(([, v]) => isEmptyFilterValue(v));
|
|
980
|
+
}
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
//#endregion
|
|
984
|
+
/**
|
|
985
|
+
* Converts AXPContextData (filter context) to AXPFilterClause format
|
|
986
|
+
*
|
|
987
|
+
* Input format:
|
|
988
|
+
* {
|
|
989
|
+
* "fieldName": {
|
|
990
|
+
* "operation": { "type": "between" },
|
|
991
|
+
* "value": { "from": "...", "to": "..." }
|
|
992
|
+
* }
|
|
993
|
+
* }
|
|
994
|
+
*
|
|
995
|
+
* Output format:
|
|
996
|
+
* {
|
|
997
|
+
* logic: 'and',
|
|
998
|
+
* filters: [
|
|
999
|
+
* {
|
|
1000
|
+
* field: 'fieldName',
|
|
1001
|
+
* operator: { type: 'between' },
|
|
1002
|
+
* value: { from: "...", to: "..." }
|
|
1003
|
+
* }
|
|
1004
|
+
* ]
|
|
1005
|
+
* }
|
|
1006
|
+
*/
|
|
1007
|
+
function convertContextDataToFilterClause(context) {
|
|
1008
|
+
const filters = [];
|
|
1009
|
+
// Iterate through all keys in context
|
|
1010
|
+
for (const [fieldName, fieldData] of Object.entries(context)) {
|
|
1011
|
+
if (fieldName.endsWith('EditorObj') || fieldName.startsWith('__')) {
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
// Drill-down and URL shortcuts may pass plain parameter values (e.g. { brand: 'id' })
|
|
1015
|
+
if (fieldData === null || fieldData === undefined || typeof fieldData !== 'object' || Array.isArray(fieldData)) {
|
|
1016
|
+
if (!isEmptyFilterValue(fieldData)) {
|
|
1017
|
+
filters.push({
|
|
1018
|
+
field: fieldName,
|
|
1019
|
+
operator: {
|
|
1020
|
+
type: Array.isArray(fieldData) ? 'in' : 'equal',
|
|
1021
|
+
},
|
|
1022
|
+
value: fieldData,
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
// Skip structured filter objects without operation metadata
|
|
1028
|
+
if (!('operation' in fieldData)) {
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const operation = fieldData.operation;
|
|
1032
|
+
const value = fieldData.value;
|
|
1033
|
+
// Skip if operation type is missing
|
|
1034
|
+
if (!operation || !operation.type) {
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
// Skip if value is effectively empty (null/undefined/empty string,
|
|
1038
|
+
// empty object/array, or deep object where all nested values are empty)
|
|
1039
|
+
if (isEmptyFilterValue(value)) {
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
// Create filter clause
|
|
1043
|
+
const filter = {
|
|
1044
|
+
field: fieldName,
|
|
1045
|
+
operator: {
|
|
1046
|
+
type: operation.type,
|
|
1047
|
+
},
|
|
1048
|
+
value: value,
|
|
1049
|
+
};
|
|
1050
|
+
filters.push(filter);
|
|
1051
|
+
}
|
|
1052
|
+
// Return filter clause with AND logic
|
|
1053
|
+
if (filters.length === 0) {
|
|
1054
|
+
return {
|
|
1055
|
+
logic: 'and',
|
|
1056
|
+
filters: [],
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
logic: 'and',
|
|
1061
|
+
filters: filters,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
//#region ---- Report Viewer User Settings Service ----
|
|
1066
|
+
class AXPReportViewerUserSettingsService {
|
|
1067
|
+
constructor() {
|
|
1068
|
+
this.settingsService = inject(AXPSettingsService);
|
|
1069
|
+
}
|
|
1070
|
+
getSettingKey(reportId) {
|
|
1071
|
+
return `ReportManagement:Report:${reportId}`;
|
|
1072
|
+
}
|
|
1073
|
+
async load(reportId) {
|
|
1074
|
+
const stored = await this.settingsService.get(this.getSettingKey(reportId));
|
|
1075
|
+
return stored ?? null;
|
|
1076
|
+
}
|
|
1077
|
+
getTableLayoutState(settings, layoutId) {
|
|
1078
|
+
return settings?.layouts?.[layoutId]?.table;
|
|
1079
|
+
}
|
|
1080
|
+
async saveActiveLayoutId(reportId, layoutId) {
|
|
1081
|
+
await this.patch(reportId, (prev) => ({
|
|
1082
|
+
...prev,
|
|
1083
|
+
activeLayoutId: layoutId,
|
|
1084
|
+
}));
|
|
1085
|
+
}
|
|
1086
|
+
async saveTableColumnOrder(reportId, layoutId, columns) {
|
|
1087
|
+
await this.patchTable(reportId, layoutId, (table) => ({
|
|
1088
|
+
...table,
|
|
1089
|
+
columns,
|
|
1090
|
+
}));
|
|
1091
|
+
}
|
|
1092
|
+
async saveTableColumnWidth(reportId, layoutId, field, width, baseColumns) {
|
|
1093
|
+
const columns = baseColumns.map((col) => col.field === field ? { ...col, width } : col);
|
|
1094
|
+
await this.saveTableColumnOrder(reportId, layoutId, columns);
|
|
1095
|
+
}
|
|
1096
|
+
async saveTableSorts(reportId, layoutId, sorts) {
|
|
1097
|
+
await this.patchTable(reportId, layoutId, (table) => ({
|
|
1098
|
+
...table,
|
|
1099
|
+
sorts,
|
|
1100
|
+
}));
|
|
1101
|
+
}
|
|
1102
|
+
async saveTablePageSize(reportId, layoutId, pageSize) {
|
|
1103
|
+
await this.patchTable(reportId, layoutId, (table) => ({
|
|
1104
|
+
...table,
|
|
1105
|
+
pageSize,
|
|
1106
|
+
}));
|
|
1107
|
+
}
|
|
1108
|
+
async patch(reportId, updateFn) {
|
|
1109
|
+
await this.settingsService.scope(AXPPlatformScope.User).update(this.getSettingKey(reportId), (prev) => {
|
|
1110
|
+
const base = (prev && typeof prev === 'object' ? prev : {});
|
|
1111
|
+
return updateFn(base);
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
async patchTable(reportId, layoutId, updateFn) {
|
|
1115
|
+
await this.patch(reportId, (prev) => {
|
|
1116
|
+
const next = { ...prev };
|
|
1117
|
+
const currentTable = prev.layouts?.[layoutId]?.table ?? {};
|
|
1118
|
+
const updatedTable = updateFn(currentTable);
|
|
1119
|
+
set(next, `layouts.${layoutId}.table`, updatedTable);
|
|
1120
|
+
return next;
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportViewerUserSettingsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1124
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportViewerUserSettingsService, providedIn: 'root' }); }
|
|
1125
|
+
}
|
|
1126
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportViewerUserSettingsService, decorators: [{
|
|
1127
|
+
type: Injectable,
|
|
1128
|
+
args: [{ providedIn: 'root' }]
|
|
1129
|
+
}] });
|
|
1130
|
+
|
|
1131
|
+
//#region ---- Constants ----
|
|
1132
|
+
const DRILL_TO_REPORT_COMMAND = 'ReportManagement.Report:DrillToReport';
|
|
1133
|
+
//#endregion
|
|
1134
|
+
//#region ---- Row Action Builder ----
|
|
1135
|
+
/**
|
|
1136
|
+
* Builds table row actions from declarative group-by drill-down definitions.
|
|
1137
|
+
* Skips row commands when linked display columns are used.
|
|
1138
|
+
*/
|
|
1139
|
+
function buildDrillRowActionsFromGroupBy(definitions, currentReportId) {
|
|
1140
|
+
if (!definitions?.length || !currentReportId || hasGroupByDrillLinks(definitions)) {
|
|
1141
|
+
return [];
|
|
1142
|
+
}
|
|
1143
|
+
const hasExplicitDefault = definitions.some((definition) => definition.default);
|
|
1144
|
+
return definitions.map((definition, index) => {
|
|
1145
|
+
const isDefault = definition.default ?? (!hasExplicitDefault && index === 0);
|
|
1146
|
+
return {
|
|
1147
|
+
title: definition.title ?? 'View',
|
|
1148
|
+
command: {
|
|
1149
|
+
name: DRILL_TO_REPORT_COMMAND,
|
|
1150
|
+
options: buildDrillCommandOptions(definition, currentReportId),
|
|
1151
|
+
},
|
|
1152
|
+
type: 'view',
|
|
1153
|
+
priority: definition.priority ?? 'primary',
|
|
1154
|
+
default: isDefault,
|
|
1155
|
+
iconOnly: false,
|
|
1156
|
+
};
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
//#endregion
|
|
1160
|
+
//#region ---- Drill Link Columns ----
|
|
1161
|
+
/**
|
|
1162
|
+
* Whether group-by definitions use linked display columns instead of row command actions.
|
|
1163
|
+
*/
|
|
1164
|
+
function hasGroupByDrillLinks(definitions) {
|
|
1165
|
+
return definitions?.some((definition) => isGroupByDrillLinkDefinition(definition)) ?? false;
|
|
1166
|
+
}
|
|
1167
|
+
function isGroupByDrillLinkDefinition(definition) {
|
|
1168
|
+
return Boolean(definition.displayField) && definition.linkDisplay !== false;
|
|
1169
|
+
}
|
|
1170
|
+
function findGroupByDefinitionForDisplayField(definitions, field) {
|
|
1171
|
+
return definitions?.find((definition) => definition.displayField === field && isGroupByDrillLinkDefinition(definition));
|
|
1172
|
+
}
|
|
1173
|
+
function buildGroupByDrillCommandOptions(definition, currentReportId) {
|
|
1174
|
+
return buildDrillCommandOptions(definition, currentReportId);
|
|
1175
|
+
}
|
|
1176
|
+
function resolveGroupByDrillLinkFields(definitions) {
|
|
1177
|
+
if (!definitions?.length) {
|
|
1178
|
+
return [];
|
|
1179
|
+
}
|
|
1180
|
+
return definitions
|
|
1181
|
+
.filter((definition) => isGroupByDrillLinkDefinition(definition))
|
|
1182
|
+
.map((definition) => definition.displayField)
|
|
1183
|
+
.filter(Boolean);
|
|
1184
|
+
}
|
|
1185
|
+
//#endregion
|
|
1186
|
+
//#region ---- Helpers ----
|
|
1187
|
+
function buildDrillCommandOptions(definition, currentReportId) {
|
|
1188
|
+
const drillDown = definition.drillDown;
|
|
1189
|
+
const reportId = drillDown.reportId ?? currentReportId;
|
|
1190
|
+
return {
|
|
1191
|
+
reportId,
|
|
1192
|
+
layoutId: drillDown.layoutId,
|
|
1193
|
+
title: resolveDrillTitle(definition, drillDown.title),
|
|
1194
|
+
parameters: mapDrillParameters(drillDown.parameters),
|
|
1195
|
+
hideColumnFields: resolveDrillHideColumnFields(definition),
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
function resolveDrillHideColumnFields(definition) {
|
|
1199
|
+
const fields = new Set(definition.fields);
|
|
1200
|
+
if (definition.displayField) {
|
|
1201
|
+
fields.add(definition.displayField);
|
|
1202
|
+
}
|
|
1203
|
+
return [...fields];
|
|
1204
|
+
}
|
|
1205
|
+
function resolveDrillTitle(definition, explicitTitle) {
|
|
1206
|
+
if (explicitTitle) {
|
|
1207
|
+
return explicitTitle;
|
|
1208
|
+
}
|
|
1209
|
+
if (definition.displayField) {
|
|
1210
|
+
return `{{ context.eval('${definition.displayField}') }}`;
|
|
1211
|
+
}
|
|
1212
|
+
return undefined;
|
|
1213
|
+
}
|
|
1214
|
+
function mapDrillParameters(parameters) {
|
|
1215
|
+
if (!parameters) {
|
|
1216
|
+
return undefined;
|
|
1217
|
+
}
|
|
1218
|
+
return Object.fromEntries(Object.entries(parameters).map(([parameterKey, rowField]) => [
|
|
1219
|
+
parameterKey,
|
|
1220
|
+
toRowExpression(rowField),
|
|
1221
|
+
]));
|
|
1222
|
+
}
|
|
1223
|
+
function toRowExpression(value) {
|
|
1224
|
+
const trimmed = value.trim();
|
|
1225
|
+
if (trimmed.includes('{{') || trimmed.includes('context.eval')) {
|
|
1226
|
+
return value;
|
|
1227
|
+
}
|
|
1228
|
+
return `{{ context.eval('${value}') }}`;
|
|
1229
|
+
}
|
|
1230
|
+
//#endregion
|
|
1231
|
+
|
|
1232
|
+
/** Infers a column display widget when the report definition omits `widget.type`. */
|
|
1233
|
+
const NUMERIC_COLUMN_FIELD_PATTERN = /(?:count|total|average|avg|min|max|price|value|amount|quantity|number|year|month|quarter|experience|rate|percent|score|age|duration|hours?|days?)$/i;
|
|
1234
|
+
function buildReportTableColumns(definitionColumns, savedColumns) {
|
|
1235
|
+
const base = definitionColumns
|
|
1236
|
+
?.filter((col) => col.visible !== false)
|
|
1237
|
+
.map((col) => mapDefinitionColumn(col)) ?? [];
|
|
1238
|
+
if (!savedColumns?.length) {
|
|
1239
|
+
return base;
|
|
1240
|
+
}
|
|
1241
|
+
const widthMap = new Map(savedColumns.map((col) => [col.field, col.width]));
|
|
1242
|
+
const orderIndex = new Map(savedColumns.map((col, index) => [col.field, index]));
|
|
1243
|
+
return [...base]
|
|
1244
|
+
.map((col) => {
|
|
1245
|
+
const savedWidth = widthMap.get(col.field);
|
|
1246
|
+
const resolvedWidth = typeof savedWidth === 'number' ? savedWidth : parseColumnResizeWidth(savedWidth) ?? col.width;
|
|
1247
|
+
return {
|
|
1248
|
+
...col,
|
|
1249
|
+
width: resolvedWidth,
|
|
1250
|
+
};
|
|
1251
|
+
})
|
|
1252
|
+
.sort((a, b) => {
|
|
1253
|
+
const aIndex = orderIndex.get(a.field) ?? Number.MAX_SAFE_INTEGER;
|
|
1254
|
+
const bIndex = orderIndex.get(b.field) ?? Number.MAX_SAFE_INTEGER;
|
|
1255
|
+
return aIndex - bIndex;
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
function toColumnUserState(columns) {
|
|
1259
|
+
return columns.map((col) => ({
|
|
1260
|
+
field: col.field,
|
|
1261
|
+
width: typeof col.width === 'number' ? col.width : parseColumnResizeWidth(col.width) ?? undefined,
|
|
1262
|
+
}));
|
|
1263
|
+
}
|
|
1264
|
+
function inferReportColumnWidgetType(field) {
|
|
1265
|
+
return NUMERIC_COLUMN_FIELD_PATTERN.test(field)
|
|
1266
|
+
? AXPWidgetsList.Editors.NumberBox
|
|
1267
|
+
: AXPWidgetsList.Editors.TextBox;
|
|
1268
|
+
}
|
|
1269
|
+
function resolveColumnWidget(col) {
|
|
1270
|
+
const path = col.widget?.path ?? col.field;
|
|
1271
|
+
if (col.widget?.type) {
|
|
1272
|
+
return { ...col.widget, path };
|
|
1273
|
+
}
|
|
1274
|
+
return {
|
|
1275
|
+
type: inferReportColumnWidgetType(col.field),
|
|
1276
|
+
path,
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
function mapDefinitionColumn(col) {
|
|
1280
|
+
return {
|
|
1281
|
+
field: col.field,
|
|
1282
|
+
title: col.title || col.field,
|
|
1283
|
+
width: col.width || 'auto',
|
|
1284
|
+
visible: col.visible !== false,
|
|
1285
|
+
align: col.align || 'left',
|
|
1286
|
+
sortable: col.sortable === true,
|
|
1287
|
+
pin: col.pin,
|
|
1288
|
+
widget: resolveColumnWidget(col),
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
function parseColumnResizeWidth(value) {
|
|
1292
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
1293
|
+
return value;
|
|
1294
|
+
}
|
|
1295
|
+
if (typeof value === 'string') {
|
|
1296
|
+
const parsed = Number.parseInt(value, 10);
|
|
1297
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1298
|
+
}
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
function resolveColumnFieldFromDataField(dataField) {
|
|
1302
|
+
if (dataField.startsWith('col-')) {
|
|
1303
|
+
return dataField.slice(4);
|
|
1304
|
+
}
|
|
1305
|
+
if (dataField.includes('-')) {
|
|
1306
|
+
return dataField.split('-').slice(1).join('-');
|
|
1307
|
+
}
|
|
1308
|
+
return dataField;
|
|
1309
|
+
}
|
|
1310
|
+
function applyGroupByDrillLinksToColumns(columns, groupByDefinitions) {
|
|
1311
|
+
if (!groupByDefinitions?.length) {
|
|
1312
|
+
return columns;
|
|
1313
|
+
}
|
|
1314
|
+
const linkFields = new Set(groupByDefinitions
|
|
1315
|
+
.filter((definition) => isGroupByDrillLinkDefinition(definition))
|
|
1316
|
+
.map((definition) => definition.displayField)
|
|
1317
|
+
.filter(Boolean));
|
|
1318
|
+
if (linkFields.size === 0) {
|
|
1319
|
+
return columns;
|
|
1320
|
+
}
|
|
1321
|
+
return columns.map((column) => {
|
|
1322
|
+
if (!linkFields.has(column.field)) {
|
|
1323
|
+
return column;
|
|
1324
|
+
}
|
|
1325
|
+
return {
|
|
1326
|
+
...column,
|
|
1327
|
+
isGroupByDrillLink: true,
|
|
1328
|
+
drillDisplayField: column.field,
|
|
1329
|
+
};
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
//#endregion
|
|
1333
|
+
|
|
1334
|
+
const AXPReportViewerViewModel = signalStore(withState(() => ({
|
|
1335
|
+
reportDefinition: null,
|
|
1336
|
+
currentLayout: null,
|
|
1337
|
+
filterContext: {},
|
|
1338
|
+
categoriesPath: [],
|
|
1339
|
+
allowFetchData: false,
|
|
1340
|
+
shouldExecuteReport: false,
|
|
1341
|
+
executionRequestId: 0,
|
|
1342
|
+
lastAppliedFilterContext: null, // Track last applied filters
|
|
1343
|
+
hasBeenExecuted: false, // Track if report has been executed at least once
|
|
1344
|
+
status: AXPPageStatus.Idle,
|
|
1345
|
+
error: null,
|
|
1346
|
+
showRowIndexColumn: false,
|
|
1347
|
+
reportUserSettings: null,
|
|
1348
|
+
})), withComputed((store, widgetRegistryService = inject(AXPWidgetRegistryService)) => ({
|
|
1349
|
+
//#region ---- Layout Management ----
|
|
1350
|
+
availableLayouts: computed(() => {
|
|
1351
|
+
const report = store.reportDefinition();
|
|
1352
|
+
if (!report?.layouts)
|
|
1353
|
+
return [];
|
|
1354
|
+
return report.layouts.map((layout) => ({
|
|
1355
|
+
id: layout.id,
|
|
1356
|
+
title: layout.title,
|
|
1357
|
+
icon: layout.icon,
|
|
1358
|
+
type: layout.layout.type,
|
|
1359
|
+
}));
|
|
1360
|
+
}),
|
|
1361
|
+
defaultLayout: computed(() => {
|
|
1362
|
+
const report = store.reportDefinition();
|
|
1363
|
+
if (!report?.layouts?.length)
|
|
1364
|
+
return null;
|
|
1365
|
+
// Find layout marked as default, or use first layout
|
|
1366
|
+
return report.layouts.find((l) => l.isDefault) || report.layouts[0];
|
|
1367
|
+
}),
|
|
1368
|
+
activeLayoutId: computed(() => store.currentLayout()?.id || null),
|
|
1369
|
+
layoutType: computed(() => store.currentLayout()?.layout?.type || null),
|
|
1370
|
+
//#endregion
|
|
1371
|
+
//#region ---- Layout-Specific Configurations ----
|
|
1372
|
+
tableLayoutConfig: computed(() => {
|
|
1373
|
+
const layout = store.currentLayout();
|
|
1374
|
+
return layout?.layout?.type === 'table' ? layout.layout : null;
|
|
1375
|
+
}),
|
|
1376
|
+
chartLayoutConfig: computed(() => {
|
|
1377
|
+
const layout = store.currentLayout();
|
|
1378
|
+
return layout?.layout?.type === 'chart' ? layout.layout : null;
|
|
1379
|
+
}),
|
|
1380
|
+
pdfLayoutConfig: computed(() => {
|
|
1381
|
+
const layout = store.currentLayout();
|
|
1382
|
+
return layout?.layout?.type === 'pdf' ? layout.layout : null;
|
|
1383
|
+
}),
|
|
1384
|
+
//#endregion
|
|
1385
|
+
//#region ---- State Indicators ----
|
|
1386
|
+
isLoading: computed(() => store.status() === AXPPageStatus.Processing),
|
|
1387
|
+
isExecuting: computed(() => store.status() === AXPPageStatus.Submitting),
|
|
1388
|
+
hasError: computed(() => store.error() !== null),
|
|
1389
|
+
canExecute: computed(() => {
|
|
1390
|
+
// Always allow execution if there's an error (to enable retry)
|
|
1391
|
+
if (store.error() !== null) {
|
|
1392
|
+
return true;
|
|
1393
|
+
}
|
|
1394
|
+
// Allow execution if there are unapplied filters
|
|
1395
|
+
const currentContext = store.filterContext();
|
|
1396
|
+
const lastApplied = store.lastAppliedFilterContext();
|
|
1397
|
+
const hasBeenExecuted = store.hasBeenExecuted();
|
|
1398
|
+
if (hasBeenExecuted && JSON.stringify(currentContext) !== JSON.stringify(lastApplied)) {
|
|
1399
|
+
return true;
|
|
1400
|
+
}
|
|
1401
|
+
// Allow execution if report hasn't been executed yet
|
|
1402
|
+
if (!hasBeenExecuted) {
|
|
1403
|
+
return true;
|
|
1404
|
+
}
|
|
1405
|
+
// Otherwise, don't allow execution
|
|
1406
|
+
return false;
|
|
1407
|
+
}),
|
|
1408
|
+
hasMultipleLayouts: computed(() => {
|
|
1409
|
+
const report = store.reportDefinition();
|
|
1410
|
+
return (report?.layouts?.length ?? 0) > 1;
|
|
1411
|
+
}),
|
|
1412
|
+
title: computed(() => store.reportDefinition()?.title ?? '@report-management:report-viewer.components.report-viewer.title'),
|
|
1413
|
+
description: computed(() => store.reportDefinition()?.description ?? null),
|
|
1414
|
+
//#endregion
|
|
1415
|
+
//#region ---- Form Configuration ----
|
|
1416
|
+
formDefinition: computed(() => {
|
|
1417
|
+
const report = store.reportDefinition();
|
|
1418
|
+
if (!report || !report.parameterGroups) {
|
|
1419
|
+
return { groups: [] };
|
|
1420
|
+
}
|
|
1421
|
+
const formDefinition = {
|
|
1422
|
+
groups: report.parameterGroups.map((group) => ({
|
|
1423
|
+
name: group.name,
|
|
1424
|
+
title: group.title,
|
|
1425
|
+
description: group.description,
|
|
1426
|
+
parameters: group.parameters.map((param) => {
|
|
1427
|
+
const editorType = param.widget.type ?? '';
|
|
1428
|
+
const widget = widgetRegistryService.getOptional(editorType);
|
|
1429
|
+
const filterType = widget?.defaultFilterWidgetName ?? editorType;
|
|
1430
|
+
// date-time / text filter aliases do not mount reliably in report filter forms;
|
|
1431
|
+
// keep the editor widget and enable filterMode instead.
|
|
1432
|
+
const useEditorFilter = editorType === 'date-time-editor' ||
|
|
1433
|
+
filterType === 'datetime-filter' ||
|
|
1434
|
+
editorType === 'text-editor' ||
|
|
1435
|
+
filterType === 'string-filter';
|
|
1436
|
+
const widgetType = useEditorFilter ? editorType || filterType : filterType;
|
|
1437
|
+
return {
|
|
1438
|
+
path: param.path,
|
|
1439
|
+
title: param.title,
|
|
1440
|
+
description: param.description,
|
|
1441
|
+
badge: `{{ filter.getTitle(context.eval("${param.path}.operation.type")) }}`,
|
|
1442
|
+
widget: {
|
|
1443
|
+
path: param.path,
|
|
1444
|
+
type: widgetType,
|
|
1445
|
+
options: {
|
|
1446
|
+
...param.widget.options,
|
|
1447
|
+
clearButton: true,
|
|
1448
|
+
hasClearButton: true,
|
|
1449
|
+
...(useEditorFilter ? { filterMode: true } : {}),
|
|
1450
|
+
},
|
|
1451
|
+
triggers: param.widget.triggers,
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
}),
|
|
1455
|
+
})),
|
|
1456
|
+
};
|
|
1457
|
+
return formDefinition;
|
|
1458
|
+
}),
|
|
1459
|
+
//#endregion
|
|
1460
|
+
//#region ---- Layout Type Detection ----
|
|
1461
|
+
isTableLayout: computed(() => store.currentLayout()?.layout?.type === 'table'),
|
|
1462
|
+
isChartLayout: computed(() => store.currentLayout()?.layout?.type === 'chart'),
|
|
1463
|
+
isPdfLayout: computed(() => store.currentLayout()?.layout?.type === 'pdf'),
|
|
1464
|
+
//#endregion
|
|
1465
|
+
//#region ---- Filter States ----
|
|
1466
|
+
hasUnappliedFilters: computed(() => {
|
|
1467
|
+
const currentContext = store.filterContext();
|
|
1468
|
+
const lastApplied = store.lastAppliedFilterContext();
|
|
1469
|
+
const hasBeenExecuted = store.hasBeenExecuted();
|
|
1470
|
+
// If report has never been executed, don't show unapplied filters warning
|
|
1471
|
+
if (!hasBeenExecuted) {
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1474
|
+
// If no filters have been applied yet, return true
|
|
1475
|
+
if (lastApplied === null) {
|
|
1476
|
+
return true;
|
|
1477
|
+
}
|
|
1478
|
+
// Check if current filters are different from last applied filters
|
|
1479
|
+
const currentKeys = Object.keys(currentContext);
|
|
1480
|
+
const lastKeys = Object.keys(lastApplied);
|
|
1481
|
+
if (currentKeys.length !== lastKeys.length) {
|
|
1482
|
+
return true;
|
|
1483
|
+
}
|
|
1484
|
+
// Check if any values are different
|
|
1485
|
+
for (const key of currentKeys) {
|
|
1486
|
+
if (currentContext[key] !== lastApplied[key]) {
|
|
1487
|
+
return true;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return false;
|
|
1491
|
+
}),
|
|
1492
|
+
//#endregion
|
|
1493
|
+
//#region ---- Action States ----
|
|
1494
|
+
canSwitchLayout: computed(() => (store.reportDefinition()?.layouts?.length ?? 0) > 1 && store.status() !== AXPPageStatus.Submitting),
|
|
1495
|
+
canSelectRows: computed(() => {
|
|
1496
|
+
const layout = store.currentLayout();
|
|
1497
|
+
return (layout?.layout?.type === 'table' &&
|
|
1498
|
+
layout.layout.options?.selectable?.mode &&
|
|
1499
|
+
layout.layout.options?.selectable?.mode !== 'none');
|
|
1500
|
+
}),
|
|
1501
|
+
tableLayoutUserSettings: computed(() => {
|
|
1502
|
+
const settings = store.reportUserSettings();
|
|
1503
|
+
const layoutId = store.currentLayout()?.id;
|
|
1504
|
+
if (!settings || !layoutId) {
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
return settings.layouts?.[layoutId]?.table ?? null;
|
|
1508
|
+
}),
|
|
1509
|
+
reportId: computed(() => store.reportDefinition()?.id ?? null),
|
|
1510
|
+
//#endregion
|
|
1511
|
+
})), withMethods((store, reportDefinitionService = inject(AXPReportViewerService), reportUserSettingsService = inject(AXPReportViewerUserSettingsService)) => {
|
|
1512
|
+
let activeFetchCount = 0;
|
|
1513
|
+
const executeReport = async () => {
|
|
1514
|
+
try {
|
|
1515
|
+
if (store.status() === AXPPageStatus.Processing ||
|
|
1516
|
+
store.status() === AXPPageStatus.Submitting) {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
const report = store.reportDefinition();
|
|
1520
|
+
const layout = store.currentLayout();
|
|
1521
|
+
if (!report || !layout) {
|
|
1522
|
+
patchState(store, {
|
|
1523
|
+
error: '@report-management:report-viewer.messages.error.report-or-layout-not-found',
|
|
1524
|
+
});
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
patchState(store, {
|
|
1528
|
+
status: AXPPageStatus.Submitting,
|
|
1529
|
+
error: null,
|
|
1530
|
+
allowFetchData: true,
|
|
1531
|
+
shouldExecuteReport: true,
|
|
1532
|
+
executionRequestId: store.executionRequestId() + 1,
|
|
1533
|
+
lastAppliedFilterContext: { ...store.filterContext() }, // Mark current filters as applied
|
|
1534
|
+
hasBeenExecuted: true, // Mark that report has been executed at least once
|
|
1535
|
+
});
|
|
1536
|
+
patchState(store, {
|
|
1537
|
+
status: AXPPageStatus.Submitted,
|
|
1538
|
+
error: null,
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
catch (error) {
|
|
1542
|
+
patchState(store, {
|
|
1543
|
+
status: AXPPageStatus.Error,
|
|
1544
|
+
error: error instanceof Error ? error.message : '@report-management:report-viewer.messages.error.unexpected-execution',
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
const fetchData = async (query) => {
|
|
1549
|
+
const report = store.reportDefinition();
|
|
1550
|
+
const layout = store.currentLayout();
|
|
1551
|
+
if (!report || !layout) {
|
|
1552
|
+
return null;
|
|
1553
|
+
}
|
|
1554
|
+
if (activeFetchCount > 0) {
|
|
1555
|
+
return null;
|
|
1556
|
+
}
|
|
1557
|
+
activeFetchCount++;
|
|
1558
|
+
patchState(store, {
|
|
1559
|
+
status: AXPPageStatus.Processing,
|
|
1560
|
+
error: null,
|
|
1561
|
+
});
|
|
1562
|
+
try {
|
|
1563
|
+
const dataSource = layout.dataSource || {
|
|
1564
|
+
type: 'query',
|
|
1565
|
+
name: 'getEmployeeList',
|
|
1566
|
+
};
|
|
1567
|
+
const filterClause = convertContextDataToFilterClause(store.filterContext());
|
|
1568
|
+
const runtimeSort = query?.sort?.length
|
|
1569
|
+
? query.sort.map((s) => ({
|
|
1570
|
+
name: s.field,
|
|
1571
|
+
dir: s.dir,
|
|
1572
|
+
}))
|
|
1573
|
+
: undefined;
|
|
1574
|
+
const context = {
|
|
1575
|
+
reportId: report.id,
|
|
1576
|
+
layoutId: layout.id,
|
|
1577
|
+
dataSource: dataSource,
|
|
1578
|
+
filters: filterClause,
|
|
1579
|
+
skip: query?.skip,
|
|
1580
|
+
take: query?.take,
|
|
1581
|
+
sort: runtimeSort,
|
|
1582
|
+
};
|
|
1583
|
+
const executionResult = await reportDefinitionService.executeReport(context);
|
|
1584
|
+
return executionResult;
|
|
1585
|
+
}
|
|
1586
|
+
catch (error) {
|
|
1587
|
+
patchState(store, {
|
|
1588
|
+
error: '@report-management:report-viewer.messages.error.failed-to-execute',
|
|
1589
|
+
});
|
|
1590
|
+
console.error('Error executing report:', error);
|
|
1591
|
+
return null;
|
|
1592
|
+
}
|
|
1593
|
+
finally {
|
|
1594
|
+
activeFetchCount--;
|
|
1595
|
+
patchState(store, {
|
|
1596
|
+
status: AXPPageStatus.Idle,
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
return {
|
|
1601
|
+
//#region ---- Report Loading ----
|
|
1602
|
+
async loadReport(reportId) {
|
|
1603
|
+
patchState(store, {
|
|
1604
|
+
status: AXPPageStatus.Processing,
|
|
1605
|
+
error: null,
|
|
1606
|
+
});
|
|
1607
|
+
try {
|
|
1608
|
+
const report = await reportDefinitionService.getReportById(reportId);
|
|
1609
|
+
if (report) {
|
|
1610
|
+
// Configure parameter widgets with proper paths
|
|
1611
|
+
report.parameterGroups.forEach((group) => {
|
|
1612
|
+
group.parameters.forEach((param) => {
|
|
1613
|
+
param.widget.path = param.path;
|
|
1614
|
+
});
|
|
1615
|
+
});
|
|
1616
|
+
// Load categories path for breadcrumbs
|
|
1617
|
+
const categoriesPath = await reportDefinitionService.getCategoriesPathByReportId(reportId);
|
|
1618
|
+
const userSettings = await reportUserSettingsService.load(reportId);
|
|
1619
|
+
const defaultLayout = report.layouts.find((l) => l.isDefault) || report.layouts[0];
|
|
1620
|
+
const savedLayout = userSettings?.activeLayoutId
|
|
1621
|
+
? report.layouts.find((l) => l.id === userSettings.activeLayoutId)
|
|
1622
|
+
: undefined;
|
|
1623
|
+
patchState(store, {
|
|
1624
|
+
reportDefinition: report,
|
|
1625
|
+
currentLayout: savedLayout ?? defaultLayout,
|
|
1626
|
+
categoriesPath: categoriesPath || [],
|
|
1627
|
+
filterContext: {},
|
|
1628
|
+
allowFetchData: false,
|
|
1629
|
+
hasBeenExecuted: false,
|
|
1630
|
+
executionRequestId: 0,
|
|
1631
|
+
reportUserSettings: userSettings,
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
catch (error) {
|
|
1636
|
+
patchState(store, {
|
|
1637
|
+
error: '@report-management:report-viewer.messages.error.failed-to-load',
|
|
1638
|
+
});
|
|
1639
|
+
console.error('Error loading report:', error);
|
|
1640
|
+
}
|
|
1641
|
+
finally {
|
|
1642
|
+
patchState(store, {
|
|
1643
|
+
status: AXPPageStatus.Idle,
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
},
|
|
1647
|
+
//#endregion
|
|
1648
|
+
//#region ---- Layout Management ----
|
|
1649
|
+
switchLayout(layoutId) {
|
|
1650
|
+
const report = store.reportDefinition();
|
|
1651
|
+
if (!report)
|
|
1652
|
+
return;
|
|
1653
|
+
const layout = report.layouts.find((l) => l.id === layoutId);
|
|
1654
|
+
if (layout) {
|
|
1655
|
+
patchState(store, {
|
|
1656
|
+
currentLayout: layout,
|
|
1657
|
+
reportUserSettings: {
|
|
1658
|
+
...(store.reportUserSettings() ?? {}),
|
|
1659
|
+
activeLayoutId: layoutId,
|
|
1660
|
+
},
|
|
1661
|
+
});
|
|
1662
|
+
void reportUserSettingsService.saveActiveLayoutId(report.id, layoutId);
|
|
1663
|
+
}
|
|
1664
|
+
},
|
|
1665
|
+
async saveTableColumnOrder(columns) {
|
|
1666
|
+
const reportId = store.reportDefinition()?.id;
|
|
1667
|
+
const layoutId = store.currentLayout()?.id;
|
|
1668
|
+
if (!reportId || !layoutId) {
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
await reportUserSettingsService.saveTableColumnOrder(reportId, layoutId, toColumnUserState(columns));
|
|
1672
|
+
patchState(store, {
|
|
1673
|
+
reportUserSettings: mergeTableColumnsIntoSettings(store.reportUserSettings(), layoutId, toColumnUserState(columns)),
|
|
1674
|
+
});
|
|
1675
|
+
},
|
|
1676
|
+
async saveTableColumnWidth(field, width, columns) {
|
|
1677
|
+
const reportId = store.reportDefinition()?.id;
|
|
1678
|
+
const layoutId = store.currentLayout()?.id;
|
|
1679
|
+
if (!reportId || !layoutId) {
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
const columnState = toColumnUserState(columns);
|
|
1683
|
+
await reportUserSettingsService.saveTableColumnWidth(reportId, layoutId, field, width, columnState);
|
|
1684
|
+
patchState(store, {
|
|
1685
|
+
reportUserSettings: mergeTableColumnsIntoSettings(store.reportUserSettings(), layoutId, columnState),
|
|
1686
|
+
});
|
|
1687
|
+
},
|
|
1688
|
+
async saveTableSorts(sorts) {
|
|
1689
|
+
const reportId = store.reportDefinition()?.id;
|
|
1690
|
+
const layoutId = store.currentLayout()?.id;
|
|
1691
|
+
if (!reportId || !layoutId) {
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
await reportUserSettingsService.saveTableSorts(reportId, layoutId, sorts);
|
|
1695
|
+
patchState(store, {
|
|
1696
|
+
reportUserSettings: mergeTableSortsIntoSettings(store.reportUserSettings(), layoutId, sorts),
|
|
1697
|
+
});
|
|
1698
|
+
},
|
|
1699
|
+
async saveTablePageSize(pageSize) {
|
|
1700
|
+
const reportId = store.reportDefinition()?.id;
|
|
1701
|
+
const layoutId = store.currentLayout()?.id;
|
|
1702
|
+
if (!reportId || !layoutId) {
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
await reportUserSettingsService.saveTablePageSize(reportId, layoutId, pageSize);
|
|
1706
|
+
patchState(store, {
|
|
1707
|
+
reportUserSettings: mergeTablePageSizeIntoSettings(store.reportUserSettings(), layoutId, pageSize),
|
|
1708
|
+
});
|
|
1709
|
+
},
|
|
1710
|
+
//#endregion
|
|
1711
|
+
//#region ---- Data Execution ----
|
|
1712
|
+
executeReport,
|
|
1713
|
+
fetchData,
|
|
1714
|
+
//#endregion
|
|
1715
|
+
//#region ---- Drill View ----
|
|
1716
|
+
/**
|
|
1717
|
+
* Applies the same layout in drill-down mode: hides group-by columns, keeps summary and data source.
|
|
1718
|
+
*/
|
|
1719
|
+
applyDrillView(options) {
|
|
1720
|
+
const layout = store.currentLayout();
|
|
1721
|
+
if (!layout || layout.layout.type !== 'table') {
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
const tableLayout = layout.layout;
|
|
1725
|
+
const hiddenFields = new Set(options.hideColumnFields);
|
|
1726
|
+
const nextTableLayout = {
|
|
1727
|
+
...tableLayout,
|
|
1728
|
+
columns: tableLayout.columns.map((column) => hiddenFields.has(column.field) ? { ...column, visible: false } : column),
|
|
1729
|
+
groupBy: undefined,
|
|
1730
|
+
actions: [],
|
|
1731
|
+
};
|
|
1732
|
+
patchState(store, {
|
|
1733
|
+
currentLayout: {
|
|
1734
|
+
...layout,
|
|
1735
|
+
layout: nextTableLayout,
|
|
1736
|
+
},
|
|
1737
|
+
});
|
|
1738
|
+
},
|
|
1739
|
+
//#endregion
|
|
1740
|
+
//#region ---- Filter Management ----
|
|
1741
|
+
updateFilterContext(context) {
|
|
1742
|
+
patchState(store, {
|
|
1743
|
+
filterContext: { ...store.filterContext(), ...context },
|
|
1744
|
+
shouldExecuteReport: false, // Reset execution flag when filters change
|
|
1745
|
+
});
|
|
1746
|
+
},
|
|
1747
|
+
clearFilters() {
|
|
1748
|
+
patchState(store, {
|
|
1749
|
+
allowFetchData: false,
|
|
1750
|
+
shouldExecuteReport: false,
|
|
1751
|
+
filterContext: {},
|
|
1752
|
+
lastAppliedFilterContext: null, // Reset applied filters when clearing
|
|
1753
|
+
hasBeenExecuted: false, // Reset execution state when clearing filters
|
|
1754
|
+
});
|
|
1755
|
+
},
|
|
1756
|
+
resetFilters() {
|
|
1757
|
+
patchState(store, {
|
|
1758
|
+
allowFetchData: false,
|
|
1759
|
+
});
|
|
1760
|
+
},
|
|
1761
|
+
//#endregion
|
|
1762
|
+
//#region ---- Export Management ----
|
|
1763
|
+
async exportReport(format) {
|
|
1764
|
+
const layout = store.currentLayout();
|
|
1765
|
+
if (!layout?.export) {
|
|
1766
|
+
console.warn('@report-management:report-viewer.messages.warnings.export-not-configured');
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
try {
|
|
1770
|
+
// Export logic would go here
|
|
1771
|
+
// This would typically call a different command for export
|
|
1772
|
+
console.log(`Exporting report in ${format} format`);
|
|
1773
|
+
}
|
|
1774
|
+
catch (error) {
|
|
1775
|
+
console.error('Error exporting report:', error);
|
|
1776
|
+
}
|
|
1777
|
+
},
|
|
1778
|
+
//#endregion
|
|
1779
|
+
//#region ---- Utility Methods ----
|
|
1780
|
+
refresh() {
|
|
1781
|
+
return executeReport();
|
|
1782
|
+
},
|
|
1783
|
+
clearError() {
|
|
1784
|
+
patchState(store, {
|
|
1785
|
+
error: null,
|
|
1786
|
+
});
|
|
1787
|
+
},
|
|
1788
|
+
clearExecutionState() {
|
|
1789
|
+
patchState(store, {
|
|
1790
|
+
hasBeenExecuted: false,
|
|
1791
|
+
allowFetchData: false,
|
|
1792
|
+
shouldExecuteReport: false,
|
|
1793
|
+
executionRequestId: 0,
|
|
1794
|
+
});
|
|
1795
|
+
},
|
|
1796
|
+
cancel() {
|
|
1797
|
+
patchState(store, {
|
|
1798
|
+
status: AXPPageStatus.Idle,
|
|
1799
|
+
hasBeenExecuted: false,
|
|
1800
|
+
error: null,
|
|
1801
|
+
});
|
|
1802
|
+
//TODO: Cancel the report execution
|
|
1803
|
+
},
|
|
1804
|
+
reset() {
|
|
1805
|
+
patchState(store, {
|
|
1806
|
+
reportDefinition: null,
|
|
1807
|
+
currentLayout: null,
|
|
1808
|
+
filterContext: {},
|
|
1809
|
+
allowFetchData: false,
|
|
1810
|
+
shouldExecuteReport: false,
|
|
1811
|
+
executionRequestId: 0,
|
|
1812
|
+
lastAppliedFilterContext: null,
|
|
1813
|
+
hasBeenExecuted: false,
|
|
1814
|
+
categoriesPath: [],
|
|
1815
|
+
status: AXPPageStatus.Idle,
|
|
1816
|
+
error: null,
|
|
1817
|
+
reportUserSettings: null,
|
|
1818
|
+
});
|
|
1819
|
+
},
|
|
1820
|
+
//#endregion
|
|
1821
|
+
};
|
|
1822
|
+
}), withHooks((store, translationService = inject(AXTranslationService), localeService = inject(AXLocaleService), settingsService = inject(AXPSettingsService)) => {
|
|
1823
|
+
const settingSubs = new Subscription();
|
|
1824
|
+
const syncShowRowIndexColumn = async () => {
|
|
1825
|
+
try {
|
|
1826
|
+
const value = await settingsService.get(AXPCommonSettings.ShowRowIndexColumn);
|
|
1827
|
+
patchState(store, { showRowIndexColumn: value ?? false });
|
|
1828
|
+
}
|
|
1829
|
+
catch {
|
|
1830
|
+
patchState(store, { showRowIndexColumn: false });
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
return {
|
|
1834
|
+
async onInit() {
|
|
1835
|
+
translationService.langChanges$.subscribe(() => {
|
|
1836
|
+
// Handle language change if needed
|
|
1837
|
+
});
|
|
1838
|
+
localeService.profileChanged$.subscribe(() => {
|
|
1839
|
+
// Handle locale change if needed
|
|
1840
|
+
});
|
|
1841
|
+
void syncShowRowIndexColumn();
|
|
1842
|
+
settingSubs.add(settingsService.onLoaded.subscribe(() => void syncShowRowIndexColumn()));
|
|
1843
|
+
settingSubs.add(settingsService.onChanged.subscribe(() => void syncShowRowIndexColumn()));
|
|
1844
|
+
},
|
|
1845
|
+
onDestroy() {
|
|
1846
|
+
settingSubs.unsubscribe();
|
|
1847
|
+
},
|
|
1848
|
+
};
|
|
1849
|
+
}));
|
|
1850
|
+
//#region ---- Settings Merge Helpers ----
|
|
1851
|
+
function mergeTableColumnsIntoSettings(settings, layoutId, columns) {
|
|
1852
|
+
const base = settings ?? {};
|
|
1853
|
+
return {
|
|
1854
|
+
...base,
|
|
1855
|
+
layouts: {
|
|
1856
|
+
...base.layouts,
|
|
1857
|
+
[layoutId]: {
|
|
1858
|
+
...base.layouts?.[layoutId],
|
|
1859
|
+
table: {
|
|
1860
|
+
...base.layouts?.[layoutId]?.table,
|
|
1861
|
+
columns,
|
|
1862
|
+
},
|
|
1863
|
+
},
|
|
1864
|
+
},
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
function mergeTableSortsIntoSettings(settings, layoutId, sorts) {
|
|
1868
|
+
const base = settings ?? {};
|
|
1869
|
+
return {
|
|
1870
|
+
...base,
|
|
1871
|
+
layouts: {
|
|
1872
|
+
...base.layouts,
|
|
1873
|
+
[layoutId]: {
|
|
1874
|
+
...base.layouts?.[layoutId],
|
|
1875
|
+
table: {
|
|
1876
|
+
...base.layouts?.[layoutId]?.table,
|
|
1877
|
+
sorts,
|
|
1878
|
+
},
|
|
1879
|
+
},
|
|
1880
|
+
},
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
function mergeTablePageSizeIntoSettings(settings, layoutId, pageSize) {
|
|
1884
|
+
const base = settings ?? {};
|
|
1885
|
+
return {
|
|
1886
|
+
...base,
|
|
1887
|
+
layouts: {
|
|
1888
|
+
...base.layouts,
|
|
1889
|
+
[layoutId]: {
|
|
1890
|
+
...base.layouts?.[layoutId],
|
|
1891
|
+
table: {
|
|
1892
|
+
...base.layouts?.[layoutId]?.table,
|
|
1893
|
+
pageSize,
|
|
1894
|
+
},
|
|
1895
|
+
},
|
|
1896
|
+
},
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
//#endregion
|
|
1900
|
+
|
|
1901
|
+
class AXPReportChartViewComponent {
|
|
1902
|
+
constructor() {
|
|
1903
|
+
//#region ---- Dependencies ----
|
|
1904
|
+
this.vm = inject(AXPReportViewerViewModel);
|
|
1905
|
+
this.AXPPageStatus = AXPPageStatus;
|
|
1906
|
+
//#endregion
|
|
1907
|
+
//#region ---- Computed Properties ----
|
|
1908
|
+
/**
|
|
1909
|
+
* Current chart layout configuration
|
|
1910
|
+
*/
|
|
1911
|
+
this.chartLayout = computed(() => {
|
|
1912
|
+
const layout = this.vm.currentLayout();
|
|
1913
|
+
return layout?.layout.type === 'chart' ? layout.layout : null;
|
|
1914
|
+
}, ...(ngDevMode ? [{ debugName: "chartLayout" }] : /* istanbul ignore next */ []));
|
|
1915
|
+
/**
|
|
1916
|
+
* Chart configuration options
|
|
1917
|
+
*/
|
|
1918
|
+
this.chartOptions = computed(() => {
|
|
1919
|
+
const chartLayout = this.chartLayout();
|
|
1920
|
+
return chartLayout?.options || null;
|
|
1921
|
+
}, ...(ngDevMode ? [{ debugName: "chartOptions" }] : /* istanbul ignore next */ []));
|
|
1922
|
+
/**
|
|
1923
|
+
* Chart type for rendering
|
|
1924
|
+
*/
|
|
1925
|
+
this.chartType = computed(() => {
|
|
1926
|
+
const options = this.chartOptions();
|
|
1927
|
+
return options?.chartType || 'bar';
|
|
1928
|
+
}, ...(ngDevMode ? [{ debugName: "chartType" }] : /* istanbul ignore next */ []));
|
|
1929
|
+
/**
|
|
1930
|
+
* Data mapping configuration
|
|
1931
|
+
*/
|
|
1932
|
+
this.dataMapping = computed(() => {
|
|
1933
|
+
const options = this.chartOptions();
|
|
1934
|
+
return options?.dataMapping || null;
|
|
1935
|
+
}, ...(ngDevMode ? [{ debugName: "dataMapping" }] : /* istanbul ignore next */ []));
|
|
1936
|
+
//#endregion
|
|
1937
|
+
//#region ---- Effects ----
|
|
1938
|
+
/**
|
|
1939
|
+
* Effect to fetch chart data when conditions are met
|
|
1940
|
+
*/
|
|
1941
|
+
this.#fetchData = effect(() => {
|
|
1942
|
+
if (this.vm.allowFetchData() && this.vm.shouldExecuteReport() && this.chartOptions()) {
|
|
1943
|
+
this.vm.fetchData().then((data) => {
|
|
1944
|
+
const raw = ((data?.data).data ?? {});
|
|
1945
|
+
switch (this.chartType()) {
|
|
1946
|
+
case 'bar': {
|
|
1947
|
+
const v = AXBarChartReportMapper(raw);
|
|
1948
|
+
this.barChartData.set(v);
|
|
1949
|
+
break;
|
|
1950
|
+
}
|
|
1951
|
+
case 'doughnut': {
|
|
1952
|
+
const v = AXPieChartReportMapper(raw);
|
|
1953
|
+
this.donutChartData.set(v);
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
case 'line':
|
|
1957
|
+
default: {
|
|
1958
|
+
const v = AXLineChartReportMapper(raw);
|
|
1959
|
+
this.lineChartData.set(v);
|
|
1960
|
+
break;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
}, ...(ngDevMode ? [{ debugName: "#fetchData" }] : /* istanbul ignore next */ []));
|
|
1966
|
+
this.legendOptions = signal({
|
|
1967
|
+
mode: 'vertical',
|
|
1968
|
+
}, ...(ngDevMode ? [{ debugName: "legendOptions" }] : /* istanbul ignore next */ []));
|
|
1969
|
+
this.barChartData = signal([], ...(ngDevMode ? [{ debugName: "barChartData" }] : /* istanbul ignore next */ []));
|
|
1970
|
+
this.barChartOptions = signal({}, ...(ngDevMode ? [{ debugName: "barChartOptions" }] : /* istanbul ignore next */ []));
|
|
1971
|
+
this.donutChartData = signal([], ...(ngDevMode ? [{ debugName: "donutChartData" }] : /* istanbul ignore next */ []));
|
|
1972
|
+
this.donutChartOptions = computed(() => {
|
|
1973
|
+
return {
|
|
1974
|
+
totalLabel: 'Total',
|
|
1975
|
+
showTooltip: true,
|
|
1976
|
+
showDataLabels: this.donutChartData().length <= 10,
|
|
1977
|
+
};
|
|
1978
|
+
}, ...(ngDevMode ? [{ debugName: "donutChartOptions" }] : /* istanbul ignore next */ []));
|
|
1979
|
+
this.lineChartData = signal([], ...(ngDevMode ? [{ debugName: "lineChartData" }] : /* istanbul ignore next */ []));
|
|
1980
|
+
this.lineChartOptions = signal({}, ...(ngDevMode ? [{ debugName: "lineChartOptions" }] : /* istanbul ignore next */ []));
|
|
1981
|
+
/**
|
|
1982
|
+
* Check if bar chart data has items
|
|
1983
|
+
*/
|
|
1984
|
+
this.hasBarChartData = computed(() => {
|
|
1985
|
+
const data = this.barChartData();
|
|
1986
|
+
return Array.isArray(data) && data.length > 0;
|
|
1987
|
+
}, ...(ngDevMode ? [{ debugName: "hasBarChartData" }] : /* istanbul ignore next */ []));
|
|
1988
|
+
/**
|
|
1989
|
+
* Check if donut chart data has items
|
|
1990
|
+
*/
|
|
1991
|
+
this.hasDonutChartData = computed(() => {
|
|
1992
|
+
const data = this.donutChartData();
|
|
1993
|
+
return Array.isArray(data) && data.length > 0;
|
|
1994
|
+
}, ...(ngDevMode ? [{ debugName: "hasDonutChartData" }] : /* istanbul ignore next */ []));
|
|
1995
|
+
/**
|
|
1996
|
+
* Check if line chart data has items
|
|
1997
|
+
*/
|
|
1998
|
+
this.hasLineChartData = computed(() => {
|
|
1999
|
+
const data = this.lineChartData();
|
|
2000
|
+
return Array.isArray(data) && data.length > 0;
|
|
2001
|
+
}, ...(ngDevMode ? [{ debugName: "hasLineChartData" }] : /* istanbul ignore next */ []));
|
|
2002
|
+
/**
|
|
2003
|
+
* Check if chart has any data (for any chart type)
|
|
2004
|
+
*/
|
|
2005
|
+
this.hasChartData = computed(() => {
|
|
2006
|
+
return this.hasBarChartData() || this.hasDonutChartData() || this.hasLineChartData();
|
|
2007
|
+
}, ...(ngDevMode ? [{ debugName: "hasChartData" }] : /* istanbul ignore next */ []));
|
|
2008
|
+
/**
|
|
2009
|
+
* Check if report has been executed and has no data
|
|
2010
|
+
*/
|
|
2011
|
+
this.showNoDataState = computed(() => {
|
|
2012
|
+
return this.vm.hasBeenExecuted() && !this.hasChartData();
|
|
2013
|
+
}, ...(ngDevMode ? [{ debugName: "showNoDataState" }] : /* istanbul ignore next */ []));
|
|
2014
|
+
}
|
|
2015
|
+
//#endregion
|
|
2016
|
+
//#region ---- Effects ----
|
|
2017
|
+
/**
|
|
2018
|
+
* Effect to fetch chart data when conditions are met
|
|
2019
|
+
*/
|
|
2020
|
+
#fetchData;
|
|
2021
|
+
//#endregion
|
|
2022
|
+
//#region ---- Helper Methods ----
|
|
2023
|
+
/**
|
|
2024
|
+
* Check if chart is ready to render
|
|
2025
|
+
*/
|
|
2026
|
+
isChartReady() {
|
|
2027
|
+
return !!(this.chartOptions() && this.dataMapping());
|
|
2028
|
+
}
|
|
2029
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportChartViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2030
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPReportChartViewComponent, isStandalone: true, selector: "axp-report-chart-view", host: { classAttribute: "axp-report-chart-view w-full h-full flex flex-col" }, ngImport: i0, template: "<ng-container *translate=\"let t\">\n @if (vm.status() === AXPPageStatus.Processing) {\n <!-- Loading State -->\n <div class=\"__loading flex flex-col items-center justify-center h-full gap-6 p-8\">\n <div class=\"spinner spin w-10 h-10\"></div>\n <div class=\"__loading-text text-sm opacity-70\">\n {{ t('@report-management:report-viewer.components.chart.messages.loading') | async }}\n </div>\n </div>\n } @else if (vm.status() === AXPPageStatus.Error) {\n <!-- Error State -->\n <axp-state-message\n mode=\"error\"\n icon=\"fa-light fa-exclamation-triangle\"\n [title]=\"'@report-management:report-viewer.messages.error.fetch-title'\"\n [description]=\"vm.error() || '@report-management:report-viewer.messages.error.fetch-description'\"\n >\n </axp-state-message>\n } @else if (isChartReady() && showNoDataState()) {\n <!-- No Data State -->\n <axp-state-message\n icon=\"fa-light fa-chart-pie\"\n [title]=\"'@report-management:report-viewer.components.chart.empty-states.no-data.title'\"\n [description]=\"'@report-management:report-viewer.components.chart.empty-states.no-data.description'\"\n >\n </axp-state-message>\n } @else if (isChartReady()) {\n <!-- Chart Content -->\n <section\n class=\"__chart-container overflow-auto md:overflow-hidden rounded-2xl border h-full flex flex-col\"\n >\n @if (chartLayout()?.header) {\n <header\n class=\"__chart-header p-4 md:p-6 border-b flex flex-col sm:flex-row gap-2 sm:items-center sm:justify-between\"\n >\n <h3 class=\"__chart-title text-base md:text-lg font-semibold truncate\">\n {{ chartLayout()?.header?.title }}\n </h3>\n @if (chartLayout()?.header?.showDate) {\n <div class=\"__chart-date text-xs md:text-sm opacity-70 font-medium flex-shrink-0\">\n {{ '@general:common.date.today' | translate | async }}\n </div>\n }\n </header>\n }\n\n <div class=\"__chart-body flex-1 p-3 md:p-6\">\n <div\n class=\"__chart-wrapper h-full items-center flex flex-col gap-4 lg:gap-6\"\n [class]=\"\n legendOptions().mode === 'horizontal'\n ? 'flex-col gap-4'\n : 'flex-col lg:flex-row gap-4 lg:gap-6'\n \"\n >\n @switch (chartType()) {\n @case ('bar') {\n <div\n class=\"__chart-content flex-1 h-full min-h-[250px] md:min-h-[300px] flex items-center justify-center w-full overflow-hidden\"\n >\n <ax-bar-chart #barChart [data]=\"barChartData()\" [options]=\"barChartOptions()\"></ax-bar-chart>\n </div>\n @if (hasBarChartData()) {\n <div class=\"__chart-legend w-fit flex-shrink-0\">\n <ax-chart-legend [options]=\"legendOptions()\" [chart]=\"barChart\"></ax-chart-legend>\n </div>\n }\n }\n @case ('doughnut') {\n <div\n class=\"__chart-content flex-1 h-full min-h-[250px] md:min-h-[300px] flex items-center justify-center w-full overflow-hidden\"\n >\n <ax-donut-chart #donutChart [data]=\"donutChartData()\" [options]=\"donutChartOptions()\"></ax-donut-chart>\n </div>\n @if (hasDonutChartData()) {\n <div class=\"__chart-legend w-fit flex-shrink-0\">\n <ax-chart-legend [options]=\"legendOptions()\" [chart]=\"donutChart\"></ax-chart-legend>\n </div>\n }\n }\n @default {\n <div\n class=\"__chart-content flex-1 h-full min-h-[250px] md:min-h-[300px] flex items-center justify-center w-full overflow-hidden\"\n >\n <ax-line-chart #lineChart [data]=\"lineChartData()\" [options]=\"lineChartOptions()\"></ax-line-chart>\n </div>\n @if (hasLineChartData()) {\n <div class=\"__chart-legend w-fit flex-shrink-0\">\n <ax-chart-legend [options]=\"legendOptions()\" [chart]=\"lineChart\"></ax-chart-legend>\n </div>\n }\n }\n }\n </div>\n </div>\n\n @if (chartLayout()?.footer) {\n <footer class=\"__chart-footer p-4 md:p-6 border-t\">\n @if (chartLayout()?.footer?.customHtml) {\n <div [innerHTML]=\"chartLayout()?.footer?.customHtml\"></div>\n }\n </footer>\n }\n </section>\n } @else {\n <!-- No Configuration State -->\n <axp-state-message\n icon=\"fa-light fa-chart-line\"\n [title]=\"'@report-management:report-viewer.components.chart.empty-states.no-config.title'\"\n [description]=\"'@report-management:report-viewer.components.chart.empty-states.no-config.description'\"\n >\n </axp-state-message>\n }\n</ng-container>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "directive", type: i1.AXTranslatorDirective, selector: "[translate]" }, { kind: "ngmodule", type: AXPWidgetCoreModule }, { kind: "component", type: AXBarChartComponent, selector: "ax-bar-chart", inputs: ["data", "options"], outputs: ["barClick"] }, { kind: "component", type: AXDonutChartComponent, selector: "ax-donut-chart", inputs: ["data", "options"], outputs: ["segmentClick", "segmentHover"] }, { kind: "component", type: AXLineChartComponent, selector: "ax-line-chart", inputs: ["data", "options"], outputs: ["pointClick"] }, { kind: "component", type: AXChartLegendComponent, selector: "ax-chart-legend", inputs: ["chart", "options"], outputs: ["itemClick", "itemMouseEnter", "itemMouseLeave"] }, { kind: "component", type: AXPStateMessageComponent, selector: "axp-state-message", inputs: ["mode", "icon", "title", "description", "titleParams", "descriptionParams", "look"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2031
|
+
}
|
|
2032
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportChartViewComponent, decorators: [{
|
|
2033
|
+
type: Component,
|
|
2034
|
+
args: [{ selector: 'axp-report-chart-view', imports: [
|
|
2035
|
+
CommonModule,
|
|
2036
|
+
AXTranslationModule,
|
|
2037
|
+
AXPWidgetCoreModule,
|
|
2038
|
+
AXBarChartComponent,
|
|
2039
|
+
AXDonutChartComponent,
|
|
2040
|
+
AXLineChartComponent,
|
|
2041
|
+
AXChartLegendComponent,
|
|
2042
|
+
AXPStateMessageComponent,
|
|
2043
|
+
], host: {
|
|
2044
|
+
class: 'axp-report-chart-view w-full h-full flex flex-col',
|
|
2045
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *translate=\"let t\">\n @if (vm.status() === AXPPageStatus.Processing) {\n <!-- Loading State -->\n <div class=\"__loading flex flex-col items-center justify-center h-full gap-6 p-8\">\n <div class=\"spinner spin w-10 h-10\"></div>\n <div class=\"__loading-text text-sm opacity-70\">\n {{ t('@report-management:report-viewer.components.chart.messages.loading') | async }}\n </div>\n </div>\n } @else if (vm.status() === AXPPageStatus.Error) {\n <!-- Error State -->\n <axp-state-message\n mode=\"error\"\n icon=\"fa-light fa-exclamation-triangle\"\n [title]=\"'@report-management:report-viewer.messages.error.fetch-title'\"\n [description]=\"vm.error() || '@report-management:report-viewer.messages.error.fetch-description'\"\n >\n </axp-state-message>\n } @else if (isChartReady() && showNoDataState()) {\n <!-- No Data State -->\n <axp-state-message\n icon=\"fa-light fa-chart-pie\"\n [title]=\"'@report-management:report-viewer.components.chart.empty-states.no-data.title'\"\n [description]=\"'@report-management:report-viewer.components.chart.empty-states.no-data.description'\"\n >\n </axp-state-message>\n } @else if (isChartReady()) {\n <!-- Chart Content -->\n <section\n class=\"__chart-container overflow-auto md:overflow-hidden rounded-2xl border h-full flex flex-col\"\n >\n @if (chartLayout()?.header) {\n <header\n class=\"__chart-header p-4 md:p-6 border-b flex flex-col sm:flex-row gap-2 sm:items-center sm:justify-between\"\n >\n <h3 class=\"__chart-title text-base md:text-lg font-semibold truncate\">\n {{ chartLayout()?.header?.title }}\n </h3>\n @if (chartLayout()?.header?.showDate) {\n <div class=\"__chart-date text-xs md:text-sm opacity-70 font-medium flex-shrink-0\">\n {{ '@general:common.date.today' | translate | async }}\n </div>\n }\n </header>\n }\n\n <div class=\"__chart-body flex-1 p-3 md:p-6\">\n <div\n class=\"__chart-wrapper h-full items-center flex flex-col gap-4 lg:gap-6\"\n [class]=\"\n legendOptions().mode === 'horizontal'\n ? 'flex-col gap-4'\n : 'flex-col lg:flex-row gap-4 lg:gap-6'\n \"\n >\n @switch (chartType()) {\n @case ('bar') {\n <div\n class=\"__chart-content flex-1 h-full min-h-[250px] md:min-h-[300px] flex items-center justify-center w-full overflow-hidden\"\n >\n <ax-bar-chart #barChart [data]=\"barChartData()\" [options]=\"barChartOptions()\"></ax-bar-chart>\n </div>\n @if (hasBarChartData()) {\n <div class=\"__chart-legend w-fit flex-shrink-0\">\n <ax-chart-legend [options]=\"legendOptions()\" [chart]=\"barChart\"></ax-chart-legend>\n </div>\n }\n }\n @case ('doughnut') {\n <div\n class=\"__chart-content flex-1 h-full min-h-[250px] md:min-h-[300px] flex items-center justify-center w-full overflow-hidden\"\n >\n <ax-donut-chart #donutChart [data]=\"donutChartData()\" [options]=\"donutChartOptions()\"></ax-donut-chart>\n </div>\n @if (hasDonutChartData()) {\n <div class=\"__chart-legend w-fit flex-shrink-0\">\n <ax-chart-legend [options]=\"legendOptions()\" [chart]=\"donutChart\"></ax-chart-legend>\n </div>\n }\n }\n @default {\n <div\n class=\"__chart-content flex-1 h-full min-h-[250px] md:min-h-[300px] flex items-center justify-center w-full overflow-hidden\"\n >\n <ax-line-chart #lineChart [data]=\"lineChartData()\" [options]=\"lineChartOptions()\"></ax-line-chart>\n </div>\n @if (hasLineChartData()) {\n <div class=\"__chart-legend w-fit flex-shrink-0\">\n <ax-chart-legend [options]=\"legendOptions()\" [chart]=\"lineChart\"></ax-chart-legend>\n </div>\n }\n }\n }\n </div>\n </div>\n\n @if (chartLayout()?.footer) {\n <footer class=\"__chart-footer p-4 md:p-6 border-t\">\n @if (chartLayout()?.footer?.customHtml) {\n <div [innerHTML]=\"chartLayout()?.footer?.customHtml\"></div>\n }\n </footer>\n }\n </section>\n } @else {\n <!-- No Configuration State -->\n <axp-state-message\n icon=\"fa-light fa-chart-line\"\n [title]=\"'@report-management:report-viewer.components.chart.empty-states.no-config.title'\"\n [description]=\"'@report-management:report-viewer.components.chart.empty-states.no-config.description'\"\n >\n </axp-state-message>\n }\n</ng-container>\n" }]
|
|
2046
|
+
}] });
|
|
2047
|
+
|
|
2048
|
+
class AXPReportPdfViewComponent {
|
|
2049
|
+
constructor() {
|
|
2050
|
+
//#region ---- Dependencies ----
|
|
2051
|
+
this.vm = inject(AXPReportViewerViewModel);
|
|
2052
|
+
this.AXPPageStatus = AXPPageStatus;
|
|
2053
|
+
this.sanitizer = inject(DomSanitizer);
|
|
2054
|
+
//#endregion
|
|
2055
|
+
//#region ---- Computed Properties ----
|
|
2056
|
+
/**
|
|
2057
|
+
* Current PDF layout configuration
|
|
2058
|
+
*/
|
|
2059
|
+
this.pdfLayout = computed(() => {
|
|
2060
|
+
const layout = this.vm.currentLayout();
|
|
2061
|
+
return layout?.layout.type === 'pdf' ? layout.layout : null;
|
|
2062
|
+
}, ...(ngDevMode ? [{ debugName: "pdfLayout" }] : /* istanbul ignore next */ []));
|
|
2063
|
+
/**
|
|
2064
|
+
* PDF layout options
|
|
2065
|
+
*/
|
|
2066
|
+
this.pdfOptions = computed(() => {
|
|
2067
|
+
const pdfLayout = this.pdfLayout();
|
|
2068
|
+
return pdfLayout?.options || {};
|
|
2069
|
+
}, ...(ngDevMode ? [{ debugName: "pdfOptions" }] : /* istanbul ignore next */ []));
|
|
2070
|
+
/**
|
|
2071
|
+
* PDF source URL (sanitized for embed)
|
|
2072
|
+
*/
|
|
2073
|
+
this.pdfSource = computed(() => {
|
|
2074
|
+
const pdfData = this.pdfData();
|
|
2075
|
+
if (!pdfData) {
|
|
2076
|
+
return null;
|
|
2077
|
+
}
|
|
2078
|
+
// Handle URL
|
|
2079
|
+
if (pdfData.url) {
|
|
2080
|
+
return this.sanitizer.bypassSecurityTrustResourceUrl(pdfData.url);
|
|
2081
|
+
}
|
|
2082
|
+
// Handle base64
|
|
2083
|
+
if (pdfData.base64) {
|
|
2084
|
+
const base64Data = pdfData.base64.startsWith('data:')
|
|
2085
|
+
? pdfData.base64
|
|
2086
|
+
: `data:application/pdf;base64,${pdfData.base64}`;
|
|
2087
|
+
return this.sanitizer.bypassSecurityTrustResourceUrl(base64Data);
|
|
2088
|
+
}
|
|
2089
|
+
// Handle blob
|
|
2090
|
+
if (pdfData.blob) {
|
|
2091
|
+
const blobUrl = URL.createObjectURL(pdfData.blob);
|
|
2092
|
+
return this.sanitizer.bypassSecurityTrustResourceUrl(blobUrl);
|
|
2093
|
+
}
|
|
2094
|
+
return null;
|
|
2095
|
+
}, ...(ngDevMode ? [{ debugName: "pdfSource" }] : /* istanbul ignore next */ []));
|
|
2096
|
+
/**
|
|
2097
|
+
* PDF filename for download
|
|
2098
|
+
*/
|
|
2099
|
+
this.pdfFilename = computed(() => {
|
|
2100
|
+
const pdfData = this.pdfData();
|
|
2101
|
+
return pdfData?.filename || 'report.pdf';
|
|
2102
|
+
}, ...(ngDevMode ? [{ debugName: "pdfFilename" }] : /* istanbul ignore next */ []));
|
|
2103
|
+
/**
|
|
2104
|
+
* PDF embed height
|
|
2105
|
+
*/
|
|
2106
|
+
this.embedHeight = computed(() => {
|
|
2107
|
+
const options = this.pdfOptions();
|
|
2108
|
+
return options.height || '100%';
|
|
2109
|
+
}, ...(ngDevMode ? [{ debugName: "embedHeight" }] : /* istanbul ignore next */ []));
|
|
2110
|
+
/**
|
|
2111
|
+
* Check if PDF is ready to render
|
|
2112
|
+
*/
|
|
2113
|
+
this.isPdfReady = computed(() => {
|
|
2114
|
+
return !!this.pdfSource();
|
|
2115
|
+
}, ...(ngDevMode ? [{ debugName: "isPdfReady" }] : /* istanbul ignore next */ []));
|
|
2116
|
+
/**
|
|
2117
|
+
* Check if download button should be shown
|
|
2118
|
+
*/
|
|
2119
|
+
this.showDownloadButton = computed(() => {
|
|
2120
|
+
const options = this.pdfOptions();
|
|
2121
|
+
return options.showDownloadButton !== false; // Default to true
|
|
2122
|
+
}, ...(ngDevMode ? [{ debugName: "showDownloadButton" }] : /* istanbul ignore next */ []));
|
|
2123
|
+
/**
|
|
2124
|
+
* Check if print button should be shown
|
|
2125
|
+
*/
|
|
2126
|
+
this.showPrintButton = computed(() => {
|
|
2127
|
+
const options = this.pdfOptions();
|
|
2128
|
+
return options.showPrintButton !== false; // Default to true
|
|
2129
|
+
}, ...(ngDevMode ? [{ debugName: "showPrintButton" }] : /* istanbul ignore next */ []));
|
|
2130
|
+
/**
|
|
2131
|
+
* Check if zoom controls should be shown
|
|
2132
|
+
*/
|
|
2133
|
+
this.showZoomControls = computed(() => {
|
|
2134
|
+
const options = this.pdfOptions();
|
|
2135
|
+
return options.showZoomControls !== false; // Default to true
|
|
2136
|
+
}, ...(ngDevMode ? [{ debugName: "showZoomControls" }] : /* istanbul ignore next */ []));
|
|
2137
|
+
/**
|
|
2138
|
+
* Check if fullscreen button should be shown
|
|
2139
|
+
*/
|
|
2140
|
+
this.showFullscreenButton = computed(() => {
|
|
2141
|
+
const options = this.pdfOptions();
|
|
2142
|
+
return options.showFullscreenButton !== false; // Default to true
|
|
2143
|
+
}, ...(ngDevMode ? [{ debugName: "showFullscreenButton" }] : /* istanbul ignore next */ []));
|
|
2144
|
+
/**
|
|
2145
|
+
* Current zoom level
|
|
2146
|
+
*/
|
|
2147
|
+
this.currentZoom = signal(100, ...(ngDevMode ? [{ debugName: "currentZoom" }] : /* istanbul ignore next */ []));
|
|
2148
|
+
/**
|
|
2149
|
+
* PDF data from execution result
|
|
2150
|
+
*/
|
|
2151
|
+
this.pdfData = signal(null, ...(ngDevMode ? [{ debugName: "pdfData" }] : /* istanbul ignore next */ []));
|
|
2152
|
+
/**
|
|
2153
|
+
* Fullscreen state
|
|
2154
|
+
*/
|
|
2155
|
+
this.isFullscreen = signal(false, ...(ngDevMode ? [{ debugName: "isFullscreen" }] : /* istanbul ignore next */ []));
|
|
2156
|
+
//#endregion
|
|
2157
|
+
//#region ---- Effects ----
|
|
2158
|
+
/**
|
|
2159
|
+
* Effect to fetch PDF data when conditions are met
|
|
2160
|
+
*/
|
|
2161
|
+
this.#fetchData = effect(() => {
|
|
2162
|
+
if (this.vm.allowFetchData() && this.vm.shouldExecuteReport() && this.pdfLayout()) {
|
|
2163
|
+
this.vm.fetchData().then((data) => {
|
|
2164
|
+
if (data?.data && data.data.type === 'pdf') {
|
|
2165
|
+
const pdfResult = data.data;
|
|
2166
|
+
this.pdfData.set(pdfResult);
|
|
2167
|
+
// Set initial zoom from options
|
|
2168
|
+
const options = this.pdfOptions();
|
|
2169
|
+
if (options.defaultZoom) {
|
|
2170
|
+
this.currentZoom.set(options.defaultZoom);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
else {
|
|
2174
|
+
this.pdfData.set(null);
|
|
2175
|
+
}
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
}, ...(ngDevMode ? [{ debugName: "#fetchData" }] : /* istanbul ignore next */ []));
|
|
2179
|
+
}
|
|
2180
|
+
//#endregion
|
|
2181
|
+
//#region ---- Effects ----
|
|
2182
|
+
/**
|
|
2183
|
+
* Effect to fetch PDF data when conditions are met
|
|
2184
|
+
*/
|
|
2185
|
+
#fetchData;
|
|
2186
|
+
//#endregion
|
|
2187
|
+
//#region ---- Methods ----
|
|
2188
|
+
/**
|
|
2189
|
+
* Handle download PDF
|
|
2190
|
+
*/
|
|
2191
|
+
downloadPdf() {
|
|
2192
|
+
const pdfData = this.pdfData();
|
|
2193
|
+
if (!pdfData) {
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
let url;
|
|
2197
|
+
let filename = this.pdfFilename();
|
|
2198
|
+
let shouldRevokeUrl = false;
|
|
2199
|
+
if (pdfData.url) {
|
|
2200
|
+
// For URL, create a temporary link
|
|
2201
|
+
url = pdfData.url;
|
|
2202
|
+
}
|
|
2203
|
+
else if (pdfData.base64) {
|
|
2204
|
+
// Convert base64 to blob and create URL
|
|
2205
|
+
const base64Data = pdfData.base64.startsWith('data:')
|
|
2206
|
+
? pdfData.base64.split(',')[1]
|
|
2207
|
+
: pdfData.base64;
|
|
2208
|
+
const byteCharacters = atob(base64Data);
|
|
2209
|
+
const byteNumbers = new Array(byteCharacters.length);
|
|
2210
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
2211
|
+
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
2212
|
+
}
|
|
2213
|
+
const byteArray = new Uint8Array(byteNumbers);
|
|
2214
|
+
const blob = new Blob([byteArray], { type: 'application/pdf' });
|
|
2215
|
+
url = URL.createObjectURL(blob);
|
|
2216
|
+
shouldRevokeUrl = true;
|
|
2217
|
+
}
|
|
2218
|
+
else if (pdfData.blob) {
|
|
2219
|
+
url = URL.createObjectURL(pdfData.blob);
|
|
2220
|
+
shouldRevokeUrl = true;
|
|
2221
|
+
}
|
|
2222
|
+
else {
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
// Create download link
|
|
2226
|
+
const link = document.createElement('a');
|
|
2227
|
+
link.href = url;
|
|
2228
|
+
link.download = filename;
|
|
2229
|
+
document.body.appendChild(link);
|
|
2230
|
+
link.click();
|
|
2231
|
+
document.body.removeChild(link);
|
|
2232
|
+
// Clean up blob URL if created
|
|
2233
|
+
if (shouldRevokeUrl) {
|
|
2234
|
+
setTimeout(() => URL.revokeObjectURL(url), 100);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Handle print PDF
|
|
2239
|
+
*/
|
|
2240
|
+
printPdf() {
|
|
2241
|
+
const pdfData = this.pdfData();
|
|
2242
|
+
if (!pdfData) {
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
let url;
|
|
2246
|
+
if (pdfData.url) {
|
|
2247
|
+
url = pdfData.url;
|
|
2248
|
+
}
|
|
2249
|
+
else if (pdfData.base64) {
|
|
2250
|
+
const base64Data = pdfData.base64.startsWith('data:')
|
|
2251
|
+
? pdfData.base64
|
|
2252
|
+
: `data:application/pdf;base64,${pdfData.base64}`;
|
|
2253
|
+
url = base64Data;
|
|
2254
|
+
}
|
|
2255
|
+
else if (pdfData.blob) {
|
|
2256
|
+
url = URL.createObjectURL(pdfData.blob);
|
|
2257
|
+
}
|
|
2258
|
+
else {
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
// Open PDF in new window for printing
|
|
2262
|
+
const printWindow = window.open(url, '_blank');
|
|
2263
|
+
if (printWindow) {
|
|
2264
|
+
printWindow.onload = () => {
|
|
2265
|
+
printWindow.print();
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Handle zoom in
|
|
2271
|
+
*/
|
|
2272
|
+
zoomIn() {
|
|
2273
|
+
const current = this.currentZoom();
|
|
2274
|
+
if (current < 500) {
|
|
2275
|
+
this.currentZoom.set(Math.min(current + 25, 500));
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Handle zoom out
|
|
2280
|
+
*/
|
|
2281
|
+
zoomOut() {
|
|
2282
|
+
const current = this.currentZoom();
|
|
2283
|
+
if (current > 25) {
|
|
2284
|
+
this.currentZoom.set(Math.max(current - 25, 25));
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* Handle zoom reset
|
|
2289
|
+
*/
|
|
2290
|
+
zoomReset() {
|
|
2291
|
+
const options = this.pdfOptions();
|
|
2292
|
+
this.currentZoom.set(options.defaultZoom || 100);
|
|
2293
|
+
}
|
|
2294
|
+
/**
|
|
2295
|
+
* Handle fullscreen toggle
|
|
2296
|
+
*/
|
|
2297
|
+
toggleFullscreen() {
|
|
2298
|
+
const isFullscreen = this.isFullscreen();
|
|
2299
|
+
this.isFullscreen.set(!isFullscreen);
|
|
2300
|
+
}
|
|
2301
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportPdfViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2302
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPReportPdfViewComponent, isStandalone: true, selector: "axp-report-pdf-view", host: { classAttribute: "axp-report-pdf-view w-full h-full flex flex-col" }, ngImport: i0, template: "<ng-container *translate=\"let t\">\n @if (vm.status() === AXPPageStatus.Processing) {\n <!-- Loading State -->\n <div class=\"__loading flex flex-col items-center justify-center h-full gap-6 p-8\">\n <div class=\"spinner spin w-10 h-10\"></div>\n <div class=\"__loading-text text-sm opacity-70\">\n {{ t('@report-management:report-viewer.components.pdf.messages.loading') | async }}\n </div>\n </div>\n } @else if (vm.status() === AXPPageStatus.Error) {\n <!-- Error State -->\n <axp-state-message mode=\"error\" icon=\"fa-light fa-exclamation-triangle\"\n [title]=\"'@report-management:report-viewer.messages.error.fetch-title'\"\n [description]=\"vm.error() || '@report-management:report-viewer.messages.error.fetch-description'\">\n </axp-state-message>\n } @else if (isPdfReady()) {\n <!-- PDF Embed Only -->\n <embed class=\"__pdf-embed w-full h-full\" [src]=\"pdfSource()\" type=\"application/pdf\" />\n } @else {\n <!-- No PDF Data State -->\n <axp-state-message icon=\"fa-light fa-file-pdf\"\n [title]=\"'@report-management:report-viewer.components.pdf.empty-states.no-data.title'\"\n [description]=\"'@report-management:report-viewer.components.pdf.empty-states.no-data.description'\">\n </axp-state-message>\n }\n</ng-container>", styles: [".axp-report-pdf-view .__loading{min-height:400px}.axp-report-pdf-view .__pdf-container{background:var(--ax-color-background)}.axp-report-pdf-view .__pdf-container.__fullscreen{position:fixed;inset:0;z-index:9999;border-radius:0}.axp-report-pdf-view .__pdf-toolbar{background:var(--ax-color-surface)}.axp-report-pdf-view .__pdf-body{background:var(--ax-color-background-secondary)}.axp-report-pdf-view .__pdf-wrapper{min-height:400px}.axp-report-pdf-view .__pdf-embed{border:none;box-shadow:0 2px 8px #0000001a;transition:width .3s ease,height .3s ease}@media(max-width:640px){.axp-report-pdf-view .__zoom-controls{width:100%;justify-content:center;margin-left:0;margin-top:.5rem}}.axp-report-pdf-view .__zoom-level{-webkit-user-select:none;user-select:none}\n/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "directive", type: i1.AXTranslatorDirective, selector: "[translate]" }, { kind: "ngmodule", type: AXPWidgetCoreModule }, { kind: "component", type: AXPStateMessageComponent, selector: "axp-state-message", inputs: ["mode", "icon", "title", "description", "titleParams", "descriptionParams", "look"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
2303
|
+
}
|
|
2304
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportPdfViewComponent, decorators: [{
|
|
2305
|
+
type: Component,
|
|
2306
|
+
args: [{ selector: 'axp-report-pdf-view', imports: [
|
|
2307
|
+
CommonModule,
|
|
2308
|
+
AXButtonModule,
|
|
2309
|
+
AXDecoratorModule,
|
|
2310
|
+
AXTranslationModule,
|
|
2311
|
+
AXPWidgetCoreModule,
|
|
2312
|
+
AXPStateMessageComponent,
|
|
2313
|
+
], host: {
|
|
2314
|
+
class: 'axp-report-pdf-view w-full h-full flex flex-col',
|
|
2315
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<ng-container *translate=\"let t\">\n @if (vm.status() === AXPPageStatus.Processing) {\n <!-- Loading State -->\n <div class=\"__loading flex flex-col items-center justify-center h-full gap-6 p-8\">\n <div class=\"spinner spin w-10 h-10\"></div>\n <div class=\"__loading-text text-sm opacity-70\">\n {{ t('@report-management:report-viewer.components.pdf.messages.loading') | async }}\n </div>\n </div>\n } @else if (vm.status() === AXPPageStatus.Error) {\n <!-- Error State -->\n <axp-state-message mode=\"error\" icon=\"fa-light fa-exclamation-triangle\"\n [title]=\"'@report-management:report-viewer.messages.error.fetch-title'\"\n [description]=\"vm.error() || '@report-management:report-viewer.messages.error.fetch-description'\">\n </axp-state-message>\n } @else if (isPdfReady()) {\n <!-- PDF Embed Only -->\n <embed class=\"__pdf-embed w-full h-full\" [src]=\"pdfSource()\" type=\"application/pdf\" />\n } @else {\n <!-- No PDF Data State -->\n <axp-state-message icon=\"fa-light fa-file-pdf\"\n [title]=\"'@report-management:report-viewer.components.pdf.empty-states.no-data.title'\"\n [description]=\"'@report-management:report-viewer.components.pdf.empty-states.no-data.description'\">\n </axp-state-message>\n }\n</ng-container>", styles: [".axp-report-pdf-view .__loading{min-height:400px}.axp-report-pdf-view .__pdf-container{background:var(--ax-color-background)}.axp-report-pdf-view .__pdf-container.__fullscreen{position:fixed;inset:0;z-index:9999;border-radius:0}.axp-report-pdf-view .__pdf-toolbar{background:var(--ax-color-surface)}.axp-report-pdf-view .__pdf-body{background:var(--ax-color-background-secondary)}.axp-report-pdf-view .__pdf-wrapper{min-height:400px}.axp-report-pdf-view .__pdf-embed{border:none;box-shadow:0 2px 8px #0000001a;transition:width .3s ease,height .3s ease}@media(max-width:640px){.axp-report-pdf-view .__zoom-controls{width:100%;justify-content:center;margin-left:0;margin-top:.5rem}}.axp-report-pdf-view .__zoom-level{-webkit-user-select:none;user-select:none}\n/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */\n"] }]
|
|
2316
|
+
}] });
|
|
2317
|
+
|
|
2318
|
+
class AXPReportTableViewComponent {
|
|
2319
|
+
constructor() {
|
|
2320
|
+
//#region ---- Dependencies ----
|
|
2321
|
+
this.vm = inject(AXPReportViewerViewModel);
|
|
2322
|
+
this.commandService = inject(AXPCommandService);
|
|
2323
|
+
this.expressionEvaluator = inject(AXPExpressionEvaluatorService);
|
|
2324
|
+
this.grid = viewChild('grid', ...(ngDevMode ? [{ debugName: "grid" }] : /* istanbul ignore next */ []));
|
|
2325
|
+
//#endregion
|
|
2326
|
+
//#region ---- State ----
|
|
2327
|
+
this.columns = signal([], ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
2328
|
+
this.sortedFields = signal([], ...(ngDevMode ? [{ debugName: "sortedFields" }] : /* istanbul ignore next */ []));
|
|
2329
|
+
this.tableSummary = signal(null, ...(ngDevMode ? [{ debugName: "tableSummary" }] : /* istanbul ignore next */ []));
|
|
2330
|
+
this.handledExecutionRequestId = 0;
|
|
2331
|
+
this.lastDataSourcePreferencesKey = null;
|
|
2332
|
+
//#endregion
|
|
2333
|
+
//#region ---- Effects ----
|
|
2334
|
+
this.#resetExecutionHandler = effect(() => {
|
|
2335
|
+
const reportId = this.vm.reportId();
|
|
2336
|
+
untracked(() => {
|
|
2337
|
+
this.handledExecutionRequestId = 0;
|
|
2338
|
+
this.lastDataSourcePreferencesKey = null;
|
|
2339
|
+
void reportId;
|
|
2340
|
+
});
|
|
2341
|
+
}, ...(ngDevMode ? [{ debugName: "#resetExecutionHandler" }] : /* istanbul ignore next */ []));
|
|
2342
|
+
this.#syncTableColumns = effect(() => {
|
|
2343
|
+
const layoutDef = this.vm.currentLayout();
|
|
2344
|
+
const tableLayout = layoutDef?.layout;
|
|
2345
|
+
if (!tableLayout || tableLayout.type !== 'table') {
|
|
2346
|
+
this.columns.set([]);
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
const savedTable = this.vm.tableLayoutUserSettings();
|
|
2350
|
+
const builtColumns = buildReportTableColumns(tableLayout.columns, savedTable?.columns);
|
|
2351
|
+
this.columns.set(applyGroupByDrillLinksToColumns(builtColumns, tableLayout.groupBy));
|
|
2352
|
+
}, ...(ngDevMode ? [{ debugName: "#syncTableColumns" }] : /* istanbul ignore next */ []));
|
|
2353
|
+
this.#syncDataSourcePreferences = effect(() => {
|
|
2354
|
+
const layoutDef = this.vm.currentLayout();
|
|
2355
|
+
if (!layoutDef) {
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
const tableLayout = layoutDef.layout;
|
|
2359
|
+
if (!tableLayout || tableLayout.type !== 'table') {
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
const savedTable = this.vm.tableLayoutUserSettings();
|
|
2363
|
+
const queryDataSource = layoutDef.dataSource;
|
|
2364
|
+
const defaultSort = tableLayout.options?.sorting?.length
|
|
2365
|
+
? tableLayout.options.sorting
|
|
2366
|
+
: (queryDataSource?.sortFields ?? []);
|
|
2367
|
+
const effectiveSort = savedTable?.sorts?.length ? savedTable.sorts : defaultSort;
|
|
2368
|
+
const pageSize = savedTable?.pageSize ?? 10;
|
|
2369
|
+
const preferencesKey = `${layoutDef.id}:${pageSize}:${JSON.stringify(effectiveSort)}`;
|
|
2370
|
+
if (this.lastDataSourcePreferencesKey === preferencesKey) {
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
untracked(() => {
|
|
2374
|
+
this.lastDataSourcePreferencesKey = preferencesKey;
|
|
2375
|
+
this.sortedFields.set(effectiveSort);
|
|
2376
|
+
this.applySortToDataSource(effectiveSort);
|
|
2377
|
+
this.dataSource.setPageSize(pageSize);
|
|
2378
|
+
});
|
|
2379
|
+
}, ...(ngDevMode ? [{ debugName: "#syncDataSourcePreferences" }] : /* istanbul ignore next */ []));
|
|
2380
|
+
/**
|
|
2381
|
+
* Refresh grid once per execute-report request.
|
|
2382
|
+
*/
|
|
2383
|
+
this.#fetchData = effect(() => {
|
|
2384
|
+
const requestId = this.vm.executionRequestId();
|
|
2385
|
+
if (requestId <= this.handledExecutionRequestId || !this.vm.allowFetchData()) {
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
const grid = this.grid();
|
|
2389
|
+
if (!grid) {
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
untracked(() => {
|
|
2393
|
+
this.handledExecutionRequestId = requestId;
|
|
2394
|
+
grid.refresh();
|
|
2395
|
+
});
|
|
2396
|
+
}, ...(ngDevMode ? [{ debugName: "#fetchData" }] : /* istanbul ignore next */ []));
|
|
2397
|
+
//#endregion
|
|
2398
|
+
//#region ---- Computed Properties ----
|
|
2399
|
+
this.layout = computed(() => {
|
|
2400
|
+
return this.vm.currentLayout()?.layout;
|
|
2401
|
+
}, ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
|
|
2402
|
+
this.dataSource = new AXDataSource({
|
|
2403
|
+
load: async (e) => {
|
|
2404
|
+
if (!this.vm.allowFetchData()) {
|
|
2405
|
+
this.tableSummary.set(null);
|
|
2406
|
+
return { items: [], total: 0 };
|
|
2407
|
+
}
|
|
2408
|
+
const result = await this.vm.fetchData(e);
|
|
2409
|
+
if (!result?.data || result.data.type !== 'table') {
|
|
2410
|
+
console.error('invalid table result');
|
|
2411
|
+
this.tableSummary.set(null);
|
|
2412
|
+
return { items: [], total: 0 };
|
|
2413
|
+
}
|
|
2414
|
+
this.tableSummary.set(result.data.summary ?? null);
|
|
2415
|
+
const items = result.data.items ?? [];
|
|
2416
|
+
const total = result.data.total || 0;
|
|
2417
|
+
return { items, total };
|
|
2418
|
+
},
|
|
2419
|
+
pageSize: 10,
|
|
2420
|
+
key: 'id',
|
|
2421
|
+
});
|
|
2422
|
+
this.visibleColumns = computed(() => this.columns().filter((c) => c.visible), ...(ngDevMode ? [{ debugName: "visibleColumns" }] : /* istanbul ignore next */ []));
|
|
2423
|
+
this.paginationEnabled = computed(() => {
|
|
2424
|
+
return this.layout().options?.pagination !== false;
|
|
2425
|
+
}, ...(ngDevMode ? [{ debugName: "paginationEnabled" }] : /* istanbul ignore next */ []));
|
|
2426
|
+
this.hasReportFooterSummary = computed(() => {
|
|
2427
|
+
return ((this.layout().summary?.some((s) => s.displayIn === 'reportFooter') ?? false) &&
|
|
2428
|
+
this.tableSummary() !== null);
|
|
2429
|
+
}, ...(ngDevMode ? [{ debugName: "hasReportFooterSummary" }] : /* istanbul ignore next */ []));
|
|
2430
|
+
this.rowActions = computed(() => {
|
|
2431
|
+
const tableLayout = this.layout();
|
|
2432
|
+
const generated = buildDrillRowActionsFromGroupBy(tableLayout.groupBy, this.vm.reportDefinition()?.id);
|
|
2433
|
+
const manual = tableLayout.actions ?? [];
|
|
2434
|
+
return [...generated, ...manual];
|
|
2435
|
+
}, ...(ngDevMode ? [{ debugName: "rowActions" }] : /* istanbul ignore next */ []));
|
|
2436
|
+
this.primaryRowAction = computed(() => {
|
|
2437
|
+
return (this.rowActions().filter((a) => a.priority === 'primary')?.map((a) => this.convertToRowCommandItem(a)) ??
|
|
2438
|
+
[]);
|
|
2439
|
+
}, ...(ngDevMode ? [{ debugName: "primaryRowAction" }] : /* istanbul ignore next */ []));
|
|
2440
|
+
this.hasPrimaryRowAction = computed(() => {
|
|
2441
|
+
return this.primaryRowAction().length > 0;
|
|
2442
|
+
}, ...(ngDevMode ? [{ debugName: "hasPrimaryRowAction" }] : /* istanbul ignore next */ []));
|
|
2443
|
+
this.secondaryRowAction = computed(() => {
|
|
2444
|
+
return (this.rowActions()
|
|
2445
|
+
.filter((a) => a.priority === 'secondary')
|
|
2446
|
+
?.map((a) => this.convertToRowCommandItem(a)) ?? []);
|
|
2447
|
+
}, ...(ngDevMode ? [{ debugName: "secondaryRowAction" }] : /* istanbul ignore next */ []));
|
|
2448
|
+
this.hasSecondaryRowAction = computed(() => {
|
|
2449
|
+
return this.secondaryRowAction().length > 0;
|
|
2450
|
+
}, ...(ngDevMode ? [{ debugName: "hasSecondaryRowAction" }] : /* istanbul ignore next */ []));
|
|
2451
|
+
this.defaultGroupByDrillDefinition = computed(() => {
|
|
2452
|
+
const groupBy = this.layout().groupBy;
|
|
2453
|
+
if (!hasGroupByDrillLinks(groupBy)) {
|
|
2454
|
+
return null;
|
|
2455
|
+
}
|
|
2456
|
+
return groupBy?.find((definition) => definition.default) ?? groupBy?.[0] ?? null;
|
|
2457
|
+
}, ...(ngDevMode ? [{ debugName: "defaultGroupByDrillDefinition" }] : /* istanbul ignore next */ []));
|
|
2458
|
+
this.defaultRowAction = computed(() => {
|
|
2459
|
+
const defaultAction = this.rowActions().find((a) => a.default);
|
|
2460
|
+
if (!defaultAction) {
|
|
2461
|
+
return null;
|
|
2462
|
+
}
|
|
2463
|
+
return this.convertToRowCommandItem(defaultAction);
|
|
2464
|
+
}, ...(ngDevMode ? [{ debugName: "defaultRowAction" }] : /* istanbul ignore next */ []));
|
|
2465
|
+
}
|
|
2466
|
+
//#endregion
|
|
2467
|
+
//#region ---- Effects ----
|
|
2468
|
+
#resetExecutionHandler;
|
|
2469
|
+
#syncTableColumns;
|
|
2470
|
+
#syncDataSourcePreferences;
|
|
2471
|
+
/**
|
|
2472
|
+
* Refresh grid once per execute-report request.
|
|
2473
|
+
*/
|
|
2474
|
+
#fetchData;
|
|
2475
|
+
//#endregion
|
|
2476
|
+
//#region ---- Event Handlers ----
|
|
2477
|
+
handleRowClick(event) {
|
|
2478
|
+
void this.handleRowAction(event.name, event.data);
|
|
2479
|
+
}
|
|
2480
|
+
handleDbRowClick(event) {
|
|
2481
|
+
const defaultGroupByDrill = this.defaultGroupByDrillDefinition();
|
|
2482
|
+
if (defaultGroupByDrill?.displayField) {
|
|
2483
|
+
void this.handleGroupByDrill(defaultGroupByDrill.displayField, event.data);
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
if (this.defaultRowAction()) {
|
|
2487
|
+
void this.handleRowAction(this.defaultRowAction().name, event.data);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
async handleGroupByDrill(displayField, data) {
|
|
2491
|
+
const definition = findGroupByDefinitionForDisplayField(this.layout().groupBy, displayField);
|
|
2492
|
+
if (!definition) {
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
const reportId = this.vm.reportDefinition()?.id;
|
|
2496
|
+
if (!reportId) {
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
const commandOptions = buildGroupByDrillCommandOptions(definition, reportId);
|
|
2500
|
+
const evaluatedOptions = await this.evaluateActionOptions(commandOptions, data);
|
|
2501
|
+
await this.commandService.execute('ReportManagement.Report:DrillToReport', {
|
|
2502
|
+
__context__: {
|
|
2503
|
+
data: data,
|
|
2504
|
+
options: evaluatedOptions,
|
|
2505
|
+
},
|
|
2506
|
+
options: evaluatedOptions,
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
async handleRowAction(name, data) {
|
|
2510
|
+
const action = this.rowActions().find((item) => item.command?.name === name);
|
|
2511
|
+
const commandName = action?.command?.name ?? name;
|
|
2512
|
+
const evaluatedOptions = await this.evaluateActionOptions(action?.command?.options, data);
|
|
2513
|
+
await this.commandService.execute(commandName, {
|
|
2514
|
+
__context__: {
|
|
2515
|
+
data: data,
|
|
2516
|
+
options: evaluatedOptions,
|
|
2517
|
+
},
|
|
2518
|
+
options: evaluatedOptions,
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
async evaluateActionOptions(options, data) {
|
|
2522
|
+
if (!options) {
|
|
2523
|
+
return {};
|
|
2524
|
+
}
|
|
2525
|
+
const scope = {
|
|
2526
|
+
context: {
|
|
2527
|
+
eval: (path) => get(data, path),
|
|
2528
|
+
},
|
|
2529
|
+
};
|
|
2530
|
+
return (await this.expressionEvaluator.evaluate(options, scope));
|
|
2531
|
+
}
|
|
2532
|
+
handleColumnSort(columnField) {
|
|
2533
|
+
const column = this.layout().columns?.find((c) => c.field === columnField);
|
|
2534
|
+
if (!column?.sortable) {
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
const activeSorts = [...this.sortedFields()];
|
|
2538
|
+
const index = activeSorts.findIndex((s) => s.name === columnField);
|
|
2539
|
+
const currentDir = index >= 0 ? activeSorts[index].dir : undefined;
|
|
2540
|
+
const newDir = currentDir === 'asc' ? 'desc' : currentDir === 'desc' ? undefined : 'asc';
|
|
2541
|
+
const nextSorts = newDir ? [{ name: columnField, dir: newDir }] : [];
|
|
2542
|
+
this.sortedFields.set(nextSorts);
|
|
2543
|
+
this.applySortToDataSource(nextSorts);
|
|
2544
|
+
void this.vm.saveTableSorts(nextSorts);
|
|
2545
|
+
this.grid()?.refresh();
|
|
2546
|
+
}
|
|
2547
|
+
onColumnSizeChanged(event) {
|
|
2548
|
+
if (!event.isUserInteraction || event.type !== 'end' || !event.data) {
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
const dataField = String(event.data.dataField ?? '');
|
|
2552
|
+
const field = resolveColumnFieldFromDataField(dataField);
|
|
2553
|
+
const width = parseColumnResizeWidth(event.data.width);
|
|
2554
|
+
if (!field || width == null) {
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
const nextColumns = this.columns().map((col) => col.field === field ? { ...col, width } : col);
|
|
2558
|
+
this.columns.set(nextColumns);
|
|
2559
|
+
void this.vm.saveTableColumnWidth(field, width, nextColumns);
|
|
2560
|
+
}
|
|
2561
|
+
onColumnsOrderChanged(event) {
|
|
2562
|
+
if (!event.isUserInteraction || !event.data?.event) {
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
const dragEvent = event.data.event;
|
|
2566
|
+
const offset = this.leadingColumnCount();
|
|
2567
|
+
const previousIndex = dragEvent.previousIndex - offset;
|
|
2568
|
+
const currentIndex = dragEvent.currentIndex - offset;
|
|
2569
|
+
if (previousIndex < 0 || currentIndex < 0) {
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
const nextColumns = [...this.columns()];
|
|
2573
|
+
const source = nextColumns[previousIndex];
|
|
2574
|
+
if (!source || source.pin) {
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
if (nextColumns[currentIndex]?.pin) {
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
moveItemInArray(nextColumns, previousIndex, currentIndex);
|
|
2581
|
+
this.columns.set(nextColumns);
|
|
2582
|
+
void this.vm.saveTableColumnOrder(nextColumns);
|
|
2583
|
+
}
|
|
2584
|
+
onPageChanged(event) {
|
|
2585
|
+
if (event.isUserInteraction) {
|
|
2586
|
+
void this.vm.saveTablePageSize(event.take);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
//#endregion
|
|
2590
|
+
//#region ---- Sort Helpers ----
|
|
2591
|
+
resolveColumnSortDirection(columnField) {
|
|
2592
|
+
const dir = this.sortedFields().find((s) => s.name === columnField)?.dir;
|
|
2593
|
+
return dir === 'asc' || dir === 'desc' ? dir : undefined;
|
|
2594
|
+
}
|
|
2595
|
+
//#endregion
|
|
2596
|
+
//#region ---- Summary Helpers ----
|
|
2597
|
+
getSummaryDisplayValue(field) {
|
|
2598
|
+
const summary = this.tableSummary();
|
|
2599
|
+
if (!summary || !(field in summary)) {
|
|
2600
|
+
return null;
|
|
2601
|
+
}
|
|
2602
|
+
const value = summary[field];
|
|
2603
|
+
if (value == null) {
|
|
2604
|
+
return null;
|
|
2605
|
+
}
|
|
2606
|
+
return String(value);
|
|
2607
|
+
}
|
|
2608
|
+
isFirstVisibleColumn(field) {
|
|
2609
|
+
return this.visibleColumns()[0]?.field === field;
|
|
2610
|
+
}
|
|
2611
|
+
//#endregion
|
|
2612
|
+
//#region ---- Helpers ----
|
|
2613
|
+
leadingColumnCount() {
|
|
2614
|
+
let count = 0;
|
|
2615
|
+
if (this.vm.showRowIndexColumn()) {
|
|
2616
|
+
count++;
|
|
2617
|
+
}
|
|
2618
|
+
if (this.vm.canSelectRows()) {
|
|
2619
|
+
count++;
|
|
2620
|
+
}
|
|
2621
|
+
return count;
|
|
2622
|
+
}
|
|
2623
|
+
applySortToDataSource(sorts) {
|
|
2624
|
+
const sortOptions = sorts.map((s) => ({ field: s.name, dir: s.dir }));
|
|
2625
|
+
this.dataSource.sort(...sortOptions);
|
|
2626
|
+
}
|
|
2627
|
+
convertToRowCommandItem(action) {
|
|
2628
|
+
const systemAction = getSystemActions(action.type);
|
|
2629
|
+
if (!systemAction) {
|
|
2630
|
+
return {
|
|
2631
|
+
icon: action.icon ?? 'fa-light fa-eye',
|
|
2632
|
+
iconOnly: action.iconOnly ?? true,
|
|
2633
|
+
text: action.title,
|
|
2634
|
+
name: action.command?.name,
|
|
2635
|
+
divided: action.break,
|
|
2636
|
+
color: action.color,
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
const defaultAction = applySystemActionDefault(action, systemAction.key);
|
|
2640
|
+
return {
|
|
2641
|
+
icon: defaultAction.icon ?? 'fa-light fa-eye',
|
|
2642
|
+
iconOnly: action.iconOnly ?? true,
|
|
2643
|
+
text: defaultAction.title,
|
|
2644
|
+
name: action.command?.name,
|
|
2645
|
+
divided: defaultAction.break ?? action.break,
|
|
2646
|
+
color: defaultAction.color,
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportTableViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2650
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPReportTableViewComponent, isStandalone: true, selector: "axp-report-table-view", host: { classAttribute: "axp-report-table-view flex flex-col flex-1 min-h-0 h-full w-full" }, viewQueries: [{ propertyName: "grid", first: true, predicate: ["grid"], descendants: true, isSignal: true }], ngImport: i0, template: "<ax-data-table\n #grid\n [dataSource]=\"dataSource\"\n [paging]=\"paginationEnabled()\"\n [showFooter]=\"hasReportFooterSummary()\"\n [allowReordering]=\"true\"\n (onRowDbClick)=\"handleDbRowClick($event)\"\n (onColumnSizeChanged)=\"onColumnSizeChanged($event)\"\n (onColumnsOrderChanged)=\"onColumnsOrderChanged($event)\"\n (onPageChanged)=\"onPageChanged($event)\"\n class=\"flex-1 min-h-0 h-full\"\n>\n @if (vm.showRowIndexColumn()) {\n <ax-index-column\n [caption]=\"('@general:terms.common.row-number' | translate | async)!\"\n fixed=\"start\"\n [width]=\"'80px'\"\n [padZero]=\"false\"\n ></ax-index-column>\n }\n @if (vm.canSelectRows()) {\n <ax-select-column fixed=\"start\" [width]=\"'50px'\"></ax-select-column>\n }\n @for (column of columns(); track column.field) {\n @if (column.visible) {\n <ng-template #footerCell>\n <div\n class=\"w-full truncate\"\n [class.text-start]=\"column.align === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-end]=\"column.align === 'right'\"\n >\n @if (isFirstVisibleColumn(column.field) && hasReportFooterSummary()) {\n <span class=\"font-medium\">\n {{ '@report-management:report-viewer.components.table.footer-total' | translate | async }}\n </span>\n } @else if (getSummaryDisplayValue(column.field); as summaryValue) {\n <span>{{ summaryValue }}</span>\n }\n </div>\n </ng-template>\n @if (column.isGroupByDrillLink && column.drillDisplayField) {\n <axp-text-command-column\n [customWidth]=\"column.width == 'auto' ? 'auto' : column.width + 'px'\"\n [caption]=\"(column.title | translate | async)!\"\n [textField]=\"column.drillDisplayField\"\n [columnFixed]=\"column.pin\"\n [sortEnabled]=\"column.sortable\"\n [headerSortDirection]=\"resolveColumnSortDirection(column.field)\"\n [footerTemplate]=\"footerCell\"\n (sortToggle)=\"handleColumnSort(column.field)\"\n (commandClick)=\"handleGroupByDrill(column.drillDisplayField, $event.data)\"\n ></axp-text-command-column>\n } @else {\n <axp-widget-column-renderer\n [customWidth]=\"column.width == 'auto' ? 'auto' : column.width + 'px'\"\n [caption]=\"(column.title | translate | async)!\"\n [node]=\"column.widget!\"\n [sortEnabled]=\"column.sortable\"\n [headerSortDirection]=\"resolveColumnSortDirection(column.field)\"\n [columnFixed]=\"column.pin\"\n [footerTemplate]=\"footerCell\"\n (sortToggle)=\"handleColumnSort(column.field)\"\n ></axp-widget-column-renderer>\n }\n }\n }\n @if (hasPrimaryRowAction()) {\n <ax-command-column\n fixed=\"end\"\n [width]=\"primaryRowAction().length * 70 + 'px'\"\n [items]=\"primaryRowAction()\"\n (onItemClick)=\"handleRowClick($event)\"\n ></ax-command-column>\n }\n @if (hasSecondaryRowAction()) {\n <ax-dropdown-command-column\n fixed=\"end\"\n [width]=\"'60px'\"\n [items]=\"secondaryRowAction()\"\n (onItemClick)=\"handleRowClick($event)\"\n ></ax-dropdown-command-column>\n }\n</ax-data-table>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXDataTableModule }, { kind: "component", type: i1$2.AXDataTableComponent, selector: "ax-data-table", inputs: ["dataSource", "selectedRows", "parentField", "hasChildrenField", "rowDetailsTemplate", "rowCssClass", "cellCssClass", "highlightText", "look", "rowTemplate", "emptyTemplate", "noDataTemplate", "alternative", "showHeader", "fixedHeader", "showFooter", "fixedFooter", "itemHeight", "allowReordering", "paging", "fetchDataMode", "loading", "focusedRow"], outputs: ["selectedRowsChange", "focusedRowChange", "onRowClick", "onRowDbClick", "onColumnsOrderChanged", "onColumnSizeChanged", "onPageChanged"] }, { kind: "component", type: i1$2.AXRowIndexColumnComponent, selector: "ax-index-column", inputs: ["width", "caption", "fixed", "footerTemplate", "padZero"] }, { kind: "component", type: i1$2.AXRowSelectColumnComponent, selector: "ax-select-column", inputs: ["width", "caption", "fixed"] }, { kind: "component", type: i1$2.AXRowCommandColumnComponent, selector: "ax-command-column", inputs: ["width", "caption", "fixed", "footerTemplate", "items"], outputs: ["onItemClick"] }, { kind: "component", type: i1$2.AXRowDropdownCommandColumnComponent, selector: "ax-dropdown-command-column", inputs: ["width", "caption", "fixed", "footerTemplate", "emptyStateTemplate", "emptyStateText", "items"], outputs: ["onItemClick"] }, { kind: "ngmodule", type: AXLoadingModule }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "ngmodule", type: AXPWidgetCoreModule }, { kind: "component", type: i2.AXPWidgetColumnRendererComponent, selector: "axp-widget-column-renderer", inputs: ["caption", "customExpandIcon", "customCollapseIcon", "customWidth", "node", "footerTemplate", "expandHandler", "sortEnabled", "headerSortDirection", "headerSortPriority", "columnFixed", "cellTemplate", "headerTemplate"], outputs: ["sortToggle"] }, { kind: "component", type: AXPTextCommandColumnComponent, selector: "axp-text-command-column", inputs: ["caption", "textField", "customWidth", "columnFixed", "footerTemplate", "sortEnabled", "headerSortDirection", "headerSortPriority", "disabled", "emptyDisplay"], outputs: ["commandClick", "sortToggle"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
2651
|
+
}
|
|
2652
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportTableViewComponent, decorators: [{
|
|
2653
|
+
type: Component,
|
|
2654
|
+
args: [{ selector: 'axp-report-table-view', imports: [
|
|
2655
|
+
CommonModule,
|
|
2656
|
+
AXDataTableModule,
|
|
2657
|
+
AXLoadingModule,
|
|
2658
|
+
AXDecoratorModule,
|
|
2659
|
+
AXTranslationModule,
|
|
2660
|
+
AXPWidgetCoreModule,
|
|
2661
|
+
AXPTextCommandColumnComponent,
|
|
2662
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
|
|
2663
|
+
class: 'axp-report-table-view flex flex-col flex-1 min-h-0 h-full w-full',
|
|
2664
|
+
}, template: "<ax-data-table\n #grid\n [dataSource]=\"dataSource\"\n [paging]=\"paginationEnabled()\"\n [showFooter]=\"hasReportFooterSummary()\"\n [allowReordering]=\"true\"\n (onRowDbClick)=\"handleDbRowClick($event)\"\n (onColumnSizeChanged)=\"onColumnSizeChanged($event)\"\n (onColumnsOrderChanged)=\"onColumnsOrderChanged($event)\"\n (onPageChanged)=\"onPageChanged($event)\"\n class=\"flex-1 min-h-0 h-full\"\n>\n @if (vm.showRowIndexColumn()) {\n <ax-index-column\n [caption]=\"('@general:terms.common.row-number' | translate | async)!\"\n fixed=\"start\"\n [width]=\"'80px'\"\n [padZero]=\"false\"\n ></ax-index-column>\n }\n @if (vm.canSelectRows()) {\n <ax-select-column fixed=\"start\" [width]=\"'50px'\"></ax-select-column>\n }\n @for (column of columns(); track column.field) {\n @if (column.visible) {\n <ng-template #footerCell>\n <div\n class=\"w-full truncate\"\n [class.text-start]=\"column.align === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-end]=\"column.align === 'right'\"\n >\n @if (isFirstVisibleColumn(column.field) && hasReportFooterSummary()) {\n <span class=\"font-medium\">\n {{ '@report-management:report-viewer.components.table.footer-total' | translate | async }}\n </span>\n } @else if (getSummaryDisplayValue(column.field); as summaryValue) {\n <span>{{ summaryValue }}</span>\n }\n </div>\n </ng-template>\n @if (column.isGroupByDrillLink && column.drillDisplayField) {\n <axp-text-command-column\n [customWidth]=\"column.width == 'auto' ? 'auto' : column.width + 'px'\"\n [caption]=\"(column.title | translate | async)!\"\n [textField]=\"column.drillDisplayField\"\n [columnFixed]=\"column.pin\"\n [sortEnabled]=\"column.sortable\"\n [headerSortDirection]=\"resolveColumnSortDirection(column.field)\"\n [footerTemplate]=\"footerCell\"\n (sortToggle)=\"handleColumnSort(column.field)\"\n (commandClick)=\"handleGroupByDrill(column.drillDisplayField, $event.data)\"\n ></axp-text-command-column>\n } @else {\n <axp-widget-column-renderer\n [customWidth]=\"column.width == 'auto' ? 'auto' : column.width + 'px'\"\n [caption]=\"(column.title | translate | async)!\"\n [node]=\"column.widget!\"\n [sortEnabled]=\"column.sortable\"\n [headerSortDirection]=\"resolveColumnSortDirection(column.field)\"\n [columnFixed]=\"column.pin\"\n [footerTemplate]=\"footerCell\"\n (sortToggle)=\"handleColumnSort(column.field)\"\n ></axp-widget-column-renderer>\n }\n }\n }\n @if (hasPrimaryRowAction()) {\n <ax-command-column\n fixed=\"end\"\n [width]=\"primaryRowAction().length * 70 + 'px'\"\n [items]=\"primaryRowAction()\"\n (onItemClick)=\"handleRowClick($event)\"\n ></ax-command-column>\n }\n @if (hasSecondaryRowAction()) {\n <ax-dropdown-command-column\n fixed=\"end\"\n [width]=\"'60px'\"\n [items]=\"secondaryRowAction()\"\n (onItemClick)=\"handleRowClick($event)\"\n ></ax-dropdown-command-column>\n }\n</ax-data-table>\n" }]
|
|
2665
|
+
}], propDecorators: { grid: [{ type: i0.ViewChild, args: ['grid', { isSignal: true }] }] } });
|
|
2666
|
+
|
|
2667
|
+
class AXPReportViewerPageComponent extends AXPPageLayoutBaseComponent {
|
|
2668
|
+
constructor() {
|
|
2669
|
+
super();
|
|
2670
|
+
//#region ---- Dependencies ----
|
|
2671
|
+
this.layoutService = inject(AXPLayoutThemeService);
|
|
2672
|
+
this.deviceService = inject(AXPDeviceService);
|
|
2673
|
+
this.route = inject(ActivatedRoute);
|
|
2674
|
+
this.unsubscriber = inject(AXUnsubscriber);
|
|
2675
|
+
this.loadingDialog = inject(AXLoadingDialogService);
|
|
2676
|
+
this.destroyRef = inject(DestroyRef);
|
|
2677
|
+
this.reportViewerService = inject(AXPReportViewerService);
|
|
2678
|
+
//#endregion
|
|
2679
|
+
//#region ---- Inputs ----
|
|
2680
|
+
this.reportId = input(...(ngDevMode ? [undefined, { debugName: "reportId" }] : /* istanbul ignore next */ []));
|
|
2681
|
+
this.reportParameters = input({}, ...(ngDevMode ? [{ debugName: "reportParameters" }] : /* istanbul ignore next */ []));
|
|
2682
|
+
//#endregion
|
|
2683
|
+
//#region ---- ViewChildren ----
|
|
2684
|
+
this.filtersForm = viewChild('filtersForm', ...(ngDevMode ? [{ debugName: "filtersForm" }] : /* istanbul ignore next */ []));
|
|
2685
|
+
//#endregion
|
|
2686
|
+
//#region ---- ViewModel ----
|
|
2687
|
+
this.vm = inject(AXPReportViewerViewModel);
|
|
2688
|
+
//#endregion
|
|
2689
|
+
//#region ---- Effects ----
|
|
2690
|
+
this.watchNavigationLoading = effect(() => {
|
|
2691
|
+
const isBusy = this.vm.isLoading() || this.vm.isExecuting();
|
|
2692
|
+
untracked(() => {
|
|
2693
|
+
this.layoutService.setNavigationLoading(isBusy);
|
|
2694
|
+
//
|
|
2695
|
+
if (isBusy) {
|
|
2696
|
+
void this.openContentLoading();
|
|
2697
|
+
}
|
|
2698
|
+
else {
|
|
2699
|
+
this.closeContentLoading();
|
|
2700
|
+
}
|
|
2701
|
+
});
|
|
2702
|
+
}, ...(ngDevMode ? [{ debugName: "watchNavigationLoading" }] : /* istanbul ignore next */ []));
|
|
2703
|
+
//#endregion
|
|
2704
|
+
this.loadingDialogRef = null;
|
|
2705
|
+
this.isDialogOpening = false;
|
|
2706
|
+
//#region ---- Computed Properties ----
|
|
2707
|
+
this.showFiltersPanel = () => this.deviceService.isLarge();
|
|
2708
|
+
this.showReportDisplay = () => this.deviceService.isLarge() || (this.deviceService.isSmall() && this.vm.hasBeenExecuted());
|
|
2709
|
+
this.baseRoutes = [this.sessionService.application?.name, 'report-management'];
|
|
2710
|
+
//#endregion
|
|
2711
|
+
//#region ---- Layout Configurations ----
|
|
2712
|
+
this.defaultLayout = () => ({
|
|
2713
|
+
positions: {
|
|
2714
|
+
sm: { colSpan: 12 },
|
|
2715
|
+
md: { colSpan: 6 },
|
|
2716
|
+
lg: { colSpan: 12 },
|
|
2717
|
+
},
|
|
2718
|
+
});
|
|
2719
|
+
this.compactLayout = () => ({
|
|
2720
|
+
positions: {
|
|
2721
|
+
sm: { colSpan: 12 },
|
|
2722
|
+
md: { colSpan: 4 },
|
|
2723
|
+
lg: { colSpan: 3 },
|
|
2724
|
+
},
|
|
2725
|
+
});
|
|
2726
|
+
this.destroyRef.onDestroy(() => this.closeContentLoading());
|
|
2727
|
+
}
|
|
2728
|
+
//#endregion
|
|
2729
|
+
//#region ---- Lifecycle ----
|
|
2730
|
+
async ngOnInit() {
|
|
2731
|
+
await super.ngOnInit();
|
|
2732
|
+
// Subscribe to route parameter changes for dynamic report loading
|
|
2733
|
+
this.unsubscriber.takeUntilDestroy(this.route.paramMap).subscribe(async (params) => {
|
|
2734
|
+
const reportId = params.get('reportId') || this.route.snapshot.queryParamMap.get('reportId') || this.reportId();
|
|
2735
|
+
if (reportId) {
|
|
2736
|
+
await this.loadReportAndRefresh(reportId);
|
|
2737
|
+
}
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
/**
|
|
2741
|
+
* Effect to handle data fetching when chart configuration changes
|
|
2742
|
+
*/
|
|
2743
|
+
//#endregion
|
|
2744
|
+
//#region ---- Helper Methods ----
|
|
2745
|
+
async loadReportAndRefresh(reportId) {
|
|
2746
|
+
await this.vm.loadReport(reportId);
|
|
2747
|
+
// Force refresh of page layout (breadcrumbs, title, etc.) after report loads
|
|
2748
|
+
this.recompute();
|
|
2749
|
+
// Handle initial parameters from query string
|
|
2750
|
+
const queryParams = this.route.snapshot.queryParams;
|
|
2751
|
+
const initialParams = { ...queryParams };
|
|
2752
|
+
delete initialParams['reportId']; // Remove reportId from parameters
|
|
2753
|
+
// Parse filter context values coming from URL (they are strings)
|
|
2754
|
+
const parsedInitialParams = this.parseFilterContextFromUrl(initialParams);
|
|
2755
|
+
// Merge with component input parameters
|
|
2756
|
+
const allParams = { ...parsedInitialParams, ...this.reportParameters() };
|
|
2757
|
+
if (Object.keys(allParams).length > 0) {
|
|
2758
|
+
console.log('Applying initial parameters:', allParams);
|
|
2759
|
+
this.vm.updateFilterContext(allParams);
|
|
2760
|
+
// Auto-execute if parameters are provided
|
|
2761
|
+
setTimeout(async () => {
|
|
2762
|
+
await this.vm.executeReport();
|
|
2763
|
+
}, 500); // Small delay to ensure form is ready
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
async openContentLoading() {
|
|
2767
|
+
if (this.loadingDialogRef || this.isDialogOpening) {
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
this.isDialogOpening = true;
|
|
2771
|
+
try {
|
|
2772
|
+
const [title, description, cancelText] = await Promise.all([
|
|
2773
|
+
this.translateService.translateAsync('@report-management:report-viewer.messages.loading.in-progress-title'),
|
|
2774
|
+
this.translateService.translateAsync('@report-management:report-viewer.messages.loading.in-progress-description'),
|
|
2775
|
+
this.translateService.translateAsync('@report-management:report-viewer.messages.loading.cancel-button'),
|
|
2776
|
+
]);
|
|
2777
|
+
this.loadingDialogRef = this.loadingDialog.show({
|
|
2778
|
+
title,
|
|
2779
|
+
mode: 'indeterminate',
|
|
2780
|
+
progressColor: 'primary',
|
|
2781
|
+
text: description,
|
|
2782
|
+
buttons: [
|
|
2783
|
+
{
|
|
2784
|
+
text: cancelText,
|
|
2785
|
+
color: 'danger',
|
|
2786
|
+
onClick: () => {
|
|
2787
|
+
this.closeContentLoading();
|
|
2788
|
+
this.vm.cancel();
|
|
2789
|
+
},
|
|
2790
|
+
},
|
|
2791
|
+
],
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
finally {
|
|
2795
|
+
this.isDialogOpening = false;
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
closeContentLoading() {
|
|
2799
|
+
if (!this.loadingDialogRef) {
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
this.loadingDialogRef.close();
|
|
2803
|
+
this.loadingDialogRef = null;
|
|
2804
|
+
}
|
|
2805
|
+
/**
|
|
2806
|
+
* Synchronizes filter context to URL query parameters using dot notation
|
|
2807
|
+
* @param filterContext The current filter context to sync
|
|
2808
|
+
*/
|
|
2809
|
+
syncFiltersToUrl(filterContext) {
|
|
2810
|
+
// Get current query params
|
|
2811
|
+
const currentParams = this.route.snapshot.queryParams;
|
|
2812
|
+
const reportId = currentParams['reportId'];
|
|
2813
|
+
// Get all filter keys from current params (excluding reportId and internal fields)
|
|
2814
|
+
const currentFilterKeys = new Set();
|
|
2815
|
+
Object.keys(currentParams).forEach((key) => {
|
|
2816
|
+
if (key !== 'reportId' &&
|
|
2817
|
+
!key.endsWith('EditorObj') &&
|
|
2818
|
+
!key.startsWith('__') &&
|
|
2819
|
+
!key.endsWith('EditorValue') &&
|
|
2820
|
+
!key.includes('.displayText')) {
|
|
2821
|
+
// Extract root key (before first dot) for filter identification
|
|
2822
|
+
const rootKey = key.includes('.') ? key.split('.')[0] : key;
|
|
2823
|
+
currentFilterKeys.add(rootKey);
|
|
2824
|
+
}
|
|
2825
|
+
});
|
|
2826
|
+
// Build new query params - start with current params and remove filter-related ones
|
|
2827
|
+
const queryParams = { ...currentParams };
|
|
2828
|
+
// Remove all filter-related params first (we'll add them back if they have values)
|
|
2829
|
+
Object.keys(queryParams).forEach((key) => {
|
|
2830
|
+
if (key !== 'reportId' &&
|
|
2831
|
+
!key.endsWith('EditorObj') &&
|
|
2832
|
+
!key.startsWith('__') &&
|
|
2833
|
+
!key.endsWith('EditorValue') &&
|
|
2834
|
+
!key.includes('.displayText')) {
|
|
2835
|
+
delete queryParams[key];
|
|
2836
|
+
}
|
|
2837
|
+
});
|
|
2838
|
+
const processedFilterKeys = new Set();
|
|
2839
|
+
// Add filter params that have values
|
|
2840
|
+
Object.keys(filterContext).forEach((key) => {
|
|
2841
|
+
// Skip internal editor fields (EditorObj, __EditorValue)
|
|
2842
|
+
if (key.endsWith('EditorObj') || key.startsWith('__') || key.endsWith('EditorValue')) {
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
processedFilterKeys.add(key);
|
|
2846
|
+
const value = filterContext[key];
|
|
2847
|
+
// Check if value is empty (null, undefined, empty string, or empty array)
|
|
2848
|
+
const isEmpty = this.isFilterValueEmpty(value);
|
|
2849
|
+
if (!isEmpty) {
|
|
2850
|
+
const flattened = this.flattenToDotNotation(key, value);
|
|
2851
|
+
Object.assign(queryParams, flattened);
|
|
2852
|
+
}
|
|
2853
|
+
// If empty, we don't add it to queryParams, so it will be removed from URL
|
|
2854
|
+
});
|
|
2855
|
+
// Remove filter keys that are no longer in filterContext
|
|
2856
|
+
currentFilterKeys.forEach((filterKey) => {
|
|
2857
|
+
if (!processedFilterKeys.has(filterKey)) {
|
|
2858
|
+
// This filter was removed - ensure all related params are removed
|
|
2859
|
+
Object.keys(currentParams).forEach((paramKey) => {
|
|
2860
|
+
if (paramKey === filterKey || paramKey.startsWith(`${filterKey}.`)) {
|
|
2861
|
+
delete queryParams[paramKey];
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
// Preserve reportId if exists
|
|
2867
|
+
if (reportId) {
|
|
2868
|
+
queryParams['reportId'] = String(reportId);
|
|
2869
|
+
}
|
|
2870
|
+
else {
|
|
2871
|
+
delete queryParams['reportId'];
|
|
2872
|
+
}
|
|
2873
|
+
// Update URL without reloading
|
|
2874
|
+
this.router.navigate([], {
|
|
2875
|
+
relativeTo: this.route,
|
|
2876
|
+
queryParams: queryParams,
|
|
2877
|
+
replaceUrl: true, // Don't add to browser history
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
/**
|
|
2881
|
+
* Checks if a filter value is considered empty
|
|
2882
|
+
*/
|
|
2883
|
+
isFilterValueEmpty(value) {
|
|
2884
|
+
if (value === null || value === undefined || value === '') {
|
|
2885
|
+
return true;
|
|
2886
|
+
}
|
|
2887
|
+
// Check if it's a filter object with empty value
|
|
2888
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
2889
|
+
const filterObj = value;
|
|
2890
|
+
const filterValue = filterObj.value;
|
|
2891
|
+
if (filterValue === null || filterValue === undefined || filterValue === '') {
|
|
2892
|
+
return true;
|
|
2893
|
+
}
|
|
2894
|
+
if (Array.isArray(filterValue) && filterValue.length === 0) {
|
|
2895
|
+
return true;
|
|
2896
|
+
}
|
|
2897
|
+
// Check nested object values
|
|
2898
|
+
if (typeof filterValue === 'object' && !Array.isArray(filterValue)) {
|
|
2899
|
+
const hasAnyValue = Object.values(filterValue).some((v) => v !== null && v !== undefined && v !== '');
|
|
2900
|
+
return !hasAnyValue;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
// Check if it's an empty array
|
|
2904
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
2905
|
+
return true;
|
|
2906
|
+
}
|
|
2907
|
+
return false;
|
|
2908
|
+
}
|
|
2909
|
+
/**
|
|
2910
|
+
* Flattens filter context to dot notation query parameters
|
|
2911
|
+
* Only includes operation.type and value - skips displayText and other fields
|
|
2912
|
+
* Example: { operation: { type: 'in' }, value: ['POST', 'GET'] }
|
|
2913
|
+
* -> { 'fieldName.operation.type': 'in', 'fieldName.value': 'POST,GET' }
|
|
2914
|
+
*/
|
|
2915
|
+
flattenToDotNotation(prefix, value) {
|
|
2916
|
+
const result = {};
|
|
2917
|
+
if (value == null) {
|
|
2918
|
+
return result;
|
|
2919
|
+
}
|
|
2920
|
+
// Handle filter context object - only extract operation.type and value
|
|
2921
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
2922
|
+
const filterObj = value;
|
|
2923
|
+
// Extract operation.type if exists
|
|
2924
|
+
if (filterObj.operation?.type) {
|
|
2925
|
+
result[`${prefix}.operation.type`] = String(filterObj.operation.type);
|
|
2926
|
+
}
|
|
2927
|
+
// Extract value - handle arrays and primitives
|
|
2928
|
+
if (filterObj.value !== undefined && filterObj.value !== null && filterObj.value !== '') {
|
|
2929
|
+
if (Array.isArray(filterObj.value)) {
|
|
2930
|
+
if (filterObj.value.length > 0) {
|
|
2931
|
+
result[`${prefix}.value`] = filterObj.value.map((v) => String(v)).join(',');
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
else if (typeof filterObj.value === 'object') {
|
|
2935
|
+
// If value is an object, flatten it recursively
|
|
2936
|
+
const flattenedValue = this.flattenValueObject(filterObj.value);
|
|
2937
|
+
Object.assign(result, flattenedValue);
|
|
2938
|
+
}
|
|
2939
|
+
else {
|
|
2940
|
+
result[`${prefix}.value`] = String(filterObj.value);
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
return result;
|
|
2944
|
+
}
|
|
2945
|
+
// Handle primitives directly
|
|
2946
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
2947
|
+
result[prefix] = String(value);
|
|
2948
|
+
return result;
|
|
2949
|
+
}
|
|
2950
|
+
// Handle arrays - join with comma
|
|
2951
|
+
if (Array.isArray(value)) {
|
|
2952
|
+
if (value.length > 0) {
|
|
2953
|
+
result[prefix] = value.map(v => String(v)).join(',');
|
|
2954
|
+
}
|
|
2955
|
+
return result;
|
|
2956
|
+
}
|
|
2957
|
+
// Fallback: convert to string
|
|
2958
|
+
result[prefix] = String(value);
|
|
2959
|
+
return result;
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Flattens a value object (for nested objects in value field)
|
|
2963
|
+
*/
|
|
2964
|
+
flattenValueObject(obj, prefix = '') {
|
|
2965
|
+
const result = {};
|
|
2966
|
+
if (obj == null || typeof obj !== 'object') {
|
|
2967
|
+
return result;
|
|
2968
|
+
}
|
|
2969
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
2970
|
+
const newPrefix = prefix ? `${prefix}.${key}` : key;
|
|
2971
|
+
if (val == null) {
|
|
2972
|
+
continue;
|
|
2973
|
+
}
|
|
2974
|
+
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
|
|
2975
|
+
result[newPrefix] = String(val);
|
|
2976
|
+
}
|
|
2977
|
+
else if (Array.isArray(val)) {
|
|
2978
|
+
if (val.length > 0) {
|
|
2979
|
+
result[newPrefix] = val.map((v) => String(v)).join(',');
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
else if (typeof val === 'object') {
|
|
2983
|
+
const flattened = this.flattenValueObject(val, newPrefix);
|
|
2984
|
+
Object.assign(result, flattened);
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
return result;
|
|
2988
|
+
}
|
|
2989
|
+
/**
|
|
2990
|
+
* Reconstructs filter context from dot notation query parameters
|
|
2991
|
+
* Only parses operation.type and value - ignores other fields
|
|
2992
|
+
* Example: { 'fieldName.operation.type': 'in', 'fieldName.value': 'POST,GET' }
|
|
2993
|
+
* -> { fieldName: { operation: { type: 'in' }, value: ['POST', 'GET'] } }
|
|
2994
|
+
*/
|
|
2995
|
+
parseFilterContextFromUrl(params) {
|
|
2996
|
+
const result = {};
|
|
2997
|
+
// Filter out reportId and internal editor fields
|
|
2998
|
+
const filteredParams = {};
|
|
2999
|
+
for (const [key, value] of Object.entries(params)) {
|
|
3000
|
+
// Skip reportId and internal editor fields (EditorObj, __EditorValue, displayText)
|
|
3001
|
+
if (key !== 'reportId' &&
|
|
3002
|
+
!key.endsWith('EditorObj') &&
|
|
3003
|
+
!key.startsWith('__') &&
|
|
3004
|
+
!key.endsWith('EditorValue') &&
|
|
3005
|
+
!key.includes('.displayText')) {
|
|
3006
|
+
filteredParams[key] = value;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
// Group by root key (before first dot)
|
|
3010
|
+
for (const [key, value] of Object.entries(filteredParams)) {
|
|
3011
|
+
if (!key.includes('.')) {
|
|
3012
|
+
// Simple key without dot notation - skip (we only want operation.type and value)
|
|
3013
|
+
continue;
|
|
3014
|
+
}
|
|
3015
|
+
// Split by dot and reconstruct nested structure
|
|
3016
|
+
const parts = key.split('.');
|
|
3017
|
+
const rootKey = parts[0];
|
|
3018
|
+
const path = parts.slice(1);
|
|
3019
|
+
// Only process operation.type and value paths
|
|
3020
|
+
if (path.length === 2 && path[0] === 'operation' && path[1] === 'type') {
|
|
3021
|
+
// operation.type
|
|
3022
|
+
if (!result[rootKey]) {
|
|
3023
|
+
result[rootKey] = { operation: {} };
|
|
3024
|
+
}
|
|
3025
|
+
if (!result[rootKey].operation) {
|
|
3026
|
+
result[rootKey].operation = {};
|
|
3027
|
+
}
|
|
3028
|
+
result[rootKey].operation.type = String(value);
|
|
3029
|
+
}
|
|
3030
|
+
else if (path.length === 1 && path[0] === 'value') {
|
|
3031
|
+
// value
|
|
3032
|
+
if (!result[rootKey]) {
|
|
3033
|
+
result[rootKey] = {};
|
|
3034
|
+
}
|
|
3035
|
+
result[rootKey].value = this.parseValue(String(value));
|
|
3036
|
+
}
|
|
3037
|
+
else if (path.length > 1 && path[0] === 'value') {
|
|
3038
|
+
// nested value object (e.g., value.from, value.to for between)
|
|
3039
|
+
if (!result[rootKey]) {
|
|
3040
|
+
result[rootKey] = {};
|
|
3041
|
+
}
|
|
3042
|
+
if (!result[rootKey].value) {
|
|
3043
|
+
result[rootKey].value = {};
|
|
3044
|
+
}
|
|
3045
|
+
const valuePath = path.slice(1);
|
|
3046
|
+
this.setNestedValue(result[rootKey].value, valuePath, this.parseValue(String(value)));
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
// Post-process: Convert values to arrays if operation.type is 'in' or 'notIn'
|
|
3050
|
+
// Also reconstruct EditorObj for lookup widgets (from value only, no displayText)
|
|
3051
|
+
for (const [key, value] of Object.entries(result)) {
|
|
3052
|
+
if (value && typeof value === 'object' && value.operation?.type) {
|
|
3053
|
+
const operationType = value.operation.type;
|
|
3054
|
+
// If operation is 'in' or 'notIn', ensure value is an array
|
|
3055
|
+
if ((operationType === 'in' || operationType === 'notIn') && typeof value.value === 'string') {
|
|
3056
|
+
// Convert comma-separated string to array
|
|
3057
|
+
value.value = value.value.split(',').map((v) => v.trim()).filter((v) => v !== '');
|
|
3058
|
+
}
|
|
3059
|
+
// Reconstruct EditorObj for lookup widgets from value only
|
|
3060
|
+
// This is needed because lookup widget expects EditorObj to exist
|
|
3061
|
+
if (value.value !== undefined && value.value !== null && value.value !== '') {
|
|
3062
|
+
const editorObjKey = `${key}EditorObj`;
|
|
3063
|
+
const valueField = 'id'; // Default value field for lookup
|
|
3064
|
+
const textField = 'title'; // Default text field for lookup
|
|
3065
|
+
if (operationType === 'in' || operationType === 'notIn') {
|
|
3066
|
+
// Multiple selection: create array of objects with value as both id and title
|
|
3067
|
+
const values = Array.isArray(value.value) ? value.value : [value.value];
|
|
3068
|
+
result[editorObjKey] = values.map((val) => ({
|
|
3069
|
+
[valueField]: val,
|
|
3070
|
+
[textField]: val // Use value as title since we don't have displayText
|
|
3071
|
+
}));
|
|
3072
|
+
}
|
|
3073
|
+
else {
|
|
3074
|
+
// Single selection: create single object
|
|
3075
|
+
result[editorObjKey] = {
|
|
3076
|
+
[valueField]: value.value,
|
|
3077
|
+
[textField]: value.value // Use value as title since we don't have displayText
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
return result;
|
|
3084
|
+
}
|
|
3085
|
+
/**
|
|
3086
|
+
* Sets a value in a nested object using a path array
|
|
3087
|
+
*/
|
|
3088
|
+
setNestedValue(obj, path, value) {
|
|
3089
|
+
let current = obj;
|
|
3090
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
3091
|
+
const key = path[i];
|
|
3092
|
+
if (!current[key] || typeof current[key] !== 'object') {
|
|
3093
|
+
current[key] = {};
|
|
3094
|
+
}
|
|
3095
|
+
current = current[key];
|
|
3096
|
+
}
|
|
3097
|
+
const lastKey = path[path.length - 1];
|
|
3098
|
+
current[lastKey] = value;
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
3101
|
+
* Parses a string value, handling arrays (comma-separated) and primitives
|
|
3102
|
+
*/
|
|
3103
|
+
parseValue(value) {
|
|
3104
|
+
if (value === '') {
|
|
3105
|
+
return value;
|
|
3106
|
+
}
|
|
3107
|
+
// Check if it's a comma-separated array
|
|
3108
|
+
if (value.includes(',')) {
|
|
3109
|
+
return value.split(',').map(v => this.parsePrimitive(v.trim()));
|
|
3110
|
+
}
|
|
3111
|
+
return this.parsePrimitive(value);
|
|
3112
|
+
}
|
|
3113
|
+
/**
|
|
3114
|
+
* Parses a primitive value
|
|
3115
|
+
* For query params, we keep everything as string to match user requirement
|
|
3116
|
+
* Arrays are handled separately in parseValue
|
|
3117
|
+
*/
|
|
3118
|
+
parsePrimitive(value) {
|
|
3119
|
+
return value;
|
|
3120
|
+
}
|
|
3121
|
+
//#endregion
|
|
3122
|
+
//#region ---- Event Handlers ----
|
|
3123
|
+
handleFiltersChange(event) {
|
|
3124
|
+
this.vm.updateFilterContext(event);
|
|
3125
|
+
this.syncFiltersToUrl(event);
|
|
3126
|
+
}
|
|
3127
|
+
handleFiltersInitiated(event) {
|
|
3128
|
+
this.vm.updateFilterContext(event);
|
|
3129
|
+
}
|
|
3130
|
+
async handleExecuteReport() {
|
|
3131
|
+
const isValid = await this.filtersForm()?.validate();
|
|
3132
|
+
if (!isValid?.result) {
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
await this.vm.executeReport();
|
|
3136
|
+
}
|
|
3137
|
+
handleRefresh() {
|
|
3138
|
+
this.vm.refresh();
|
|
3139
|
+
}
|
|
3140
|
+
handleClearFilters() {
|
|
3141
|
+
this.vm.clearFilters();
|
|
3142
|
+
// Reset form if available
|
|
3143
|
+
const form = this.filtersForm();
|
|
3144
|
+
if (form) {
|
|
3145
|
+
form.reset();
|
|
3146
|
+
}
|
|
3147
|
+
// Clear URL query parameters (preserve reportId if exists)
|
|
3148
|
+
const currentParams = this.route.snapshot.queryParams;
|
|
3149
|
+
const reportId = currentParams['reportId'];
|
|
3150
|
+
this.router.navigate([], {
|
|
3151
|
+
relativeTo: this.route,
|
|
3152
|
+
queryParams: reportId ? { reportId } : {},
|
|
3153
|
+
replaceUrl: true,
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
/**
|
|
3157
|
+
* Handle download report action
|
|
3158
|
+
* Gets PDF data from viewmodel and calls service executeAction method
|
|
3159
|
+
*/
|
|
3160
|
+
async handleDownloadReport() {
|
|
3161
|
+
if (!this.vm.isPdfLayout()) {
|
|
3162
|
+
const warning = await this.translateService.translateAsync('@report-management:report-viewer.messages.warnings.download-pdf-only');
|
|
3163
|
+
console.warn(warning);
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
// Fetch PDF data if not already available
|
|
3167
|
+
const result = await this.vm.fetchData();
|
|
3168
|
+
if (result?.data && result.data.type === 'pdf') {
|
|
3169
|
+
const pdfData = result.data;
|
|
3170
|
+
await this.reportViewerService.executeAction('download-report', { pdfData });
|
|
3171
|
+
}
|
|
3172
|
+
else {
|
|
3173
|
+
const warning = await this.translateService.translateAsync('@report-management:report-viewer.messages.warnings.no-pdf-download');
|
|
3174
|
+
console.warn(warning);
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
/**
|
|
3178
|
+
* Handle print report action
|
|
3179
|
+
* Gets PDF data from viewmodel and calls service executeAction method
|
|
3180
|
+
*/
|
|
3181
|
+
async handlePrintReport() {
|
|
3182
|
+
if (!this.vm.isPdfLayout()) {
|
|
3183
|
+
const warning = await this.translateService.translateAsync('@report-management:report-viewer.messages.warnings.print-pdf-only');
|
|
3184
|
+
console.warn(warning);
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
3187
|
+
// Fetch PDF data if not already available
|
|
3188
|
+
const result = await this.vm.fetchData();
|
|
3189
|
+
if (result?.data && result.data.type === 'pdf') {
|
|
3190
|
+
const pdfData = result.data;
|
|
3191
|
+
await this.reportViewerService.executeAction('print-report', { pdfData });
|
|
3192
|
+
}
|
|
3193
|
+
else {
|
|
3194
|
+
const warning = await this.translateService.translateAsync('@report-management:report-viewer.messages.warnings.no-pdf-print');
|
|
3195
|
+
console.warn(warning);
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
/**
|
|
3199
|
+
* Handle export report action
|
|
3200
|
+
* Gets report data from viewmodel and calls service executeAction method
|
|
3201
|
+
* @param format - Export format (excel, csv, pdf, image)
|
|
3202
|
+
*/
|
|
3203
|
+
async handleExportReport(format) {
|
|
3204
|
+
if (!format) {
|
|
3205
|
+
const warning = await this.translateService.translateAsync('@report-management:report-viewer.messages.warnings.export-format-required');
|
|
3206
|
+
console.warn(warning);
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
// Fetch report data
|
|
3210
|
+
const result = await this.vm.fetchData();
|
|
3211
|
+
if (!result?.data) {
|
|
3212
|
+
const warning = await this.translateService.translateAsync('@report-management:report-viewer.messages.warnings.no-report-data-export');
|
|
3213
|
+
console.warn(warning);
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
const layout = this.vm.currentLayout();
|
|
3217
|
+
await this.reportViewerService.executeAction('export-report', {
|
|
3218
|
+
format,
|
|
3219
|
+
reportData: result.data,
|
|
3220
|
+
layoutDefinition: layout || undefined,
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
//#endregion
|
|
3224
|
+
//#region ---- Page Configuration Overrides ----
|
|
3225
|
+
async getPageBreadcrumbs() {
|
|
3226
|
+
const breadcrumbs = [
|
|
3227
|
+
{
|
|
3228
|
+
name: 'reports',
|
|
3229
|
+
title: await this.translateService.translateAsync('@report-management:module.menus.root.title'),
|
|
3230
|
+
command: {
|
|
3231
|
+
name: 'navigate-to-reports',
|
|
3232
|
+
},
|
|
3233
|
+
},
|
|
3234
|
+
];
|
|
3235
|
+
// Add categories path
|
|
3236
|
+
const categoriesPath = this.vm.categoriesPath();
|
|
3237
|
+
if (categoriesPath && categoriesPath.length > 0) {
|
|
3238
|
+
let urlParts = [...this.baseRoutes, 'reports'];
|
|
3239
|
+
for (const category of categoriesPath) {
|
|
3240
|
+
urlParts.push(category.id);
|
|
3241
|
+
breadcrumbs.push({
|
|
3242
|
+
name: category.id,
|
|
3243
|
+
title: category.title,
|
|
3244
|
+
command: {
|
|
3245
|
+
name: 'navigate-to-category',
|
|
3246
|
+
options: {
|
|
3247
|
+
path: [...urlParts],
|
|
3248
|
+
},
|
|
3249
|
+
},
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
// Add report title as final breadcrumb
|
|
3254
|
+
const reportDefinition = this.vm.reportDefinition();
|
|
3255
|
+
if (reportDefinition) {
|
|
3256
|
+
breadcrumbs.push({
|
|
3257
|
+
name: 'report-viewer',
|
|
3258
|
+
title: reportDefinition.title,
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
3261
|
+
return breadcrumbs;
|
|
3262
|
+
}
|
|
3263
|
+
async getPageTitle() {
|
|
3264
|
+
const reportDefinition = this.vm.reportDefinition();
|
|
3265
|
+
if (reportDefinition) {
|
|
3266
|
+
return reportDefinition.title;
|
|
3267
|
+
}
|
|
3268
|
+
return await this.translateService.translateAsync('@report-management:report-viewer.components.report-viewer.title');
|
|
3269
|
+
}
|
|
3270
|
+
async getPageDescription() {
|
|
3271
|
+
const reportDefinition = this.vm.reportDefinition();
|
|
3272
|
+
return reportDefinition?.description || null;
|
|
3273
|
+
}
|
|
3274
|
+
getPageIcon() {
|
|
3275
|
+
// Show current layout icon before the title when there are multiple layouts
|
|
3276
|
+
if (!this.vm.hasMultipleLayouts() || !this.showReportDisplay()) {
|
|
3277
|
+
return null;
|
|
3278
|
+
}
|
|
3279
|
+
const currentLayout = this.vm.currentLayout();
|
|
3280
|
+
if (currentLayout?.icon) {
|
|
3281
|
+
return `fa-light ${currentLayout.icon}`;
|
|
3282
|
+
}
|
|
3283
|
+
return null;
|
|
3284
|
+
}
|
|
3285
|
+
getTitleActions() {
|
|
3286
|
+
// Only show layout switcher if there are multiple layouts and report display is visible
|
|
3287
|
+
if (!this.vm.hasMultipleLayouts() || !this.showReportDisplay()) {
|
|
3288
|
+
return [];
|
|
3289
|
+
}
|
|
3290
|
+
const currentLayoutId = this.vm.currentLayout()?.id;
|
|
3291
|
+
return this.vm
|
|
3292
|
+
.availableLayouts()
|
|
3293
|
+
.map((layout) => ({
|
|
3294
|
+
title: layout.title,
|
|
3295
|
+
// Always show layout icon in prefix
|
|
3296
|
+
icon: layout.icon ? `fa-light ${layout.icon}` : undefined,
|
|
3297
|
+
command: {
|
|
3298
|
+
name: 'switch-layout',
|
|
3299
|
+
metadata: { isLayout: true, layoutId: layout.id, isSelected: layout.id === currentLayoutId },
|
|
3300
|
+
},
|
|
3301
|
+
}))
|
|
3302
|
+
.sort((a, b) => (a.title ?? '').localeCompare(b.title ?? '', undefined, { sensitivity: 'base' }));
|
|
3303
|
+
}
|
|
3304
|
+
async getBackButton() {
|
|
3305
|
+
// On mobile, if report has been executed (showing report), show "Filters" as back button
|
|
3306
|
+
if (this.deviceService.isSmall() && this.vm.hasBeenExecuted()) {
|
|
3307
|
+
return {
|
|
3308
|
+
title: await this.translateService.translateAsync('@general:terms.interface.filter.title'),
|
|
3309
|
+
};
|
|
3310
|
+
}
|
|
3311
|
+
const categoriesPath = this.vm.categoriesPath();
|
|
3312
|
+
if (!categoriesPath || categoriesPath.length === 0) {
|
|
3313
|
+
// If no categories path, go back to reports root
|
|
3314
|
+
return {
|
|
3315
|
+
title: await this.translateService.translateAsync('@report-management:report-runner.terms.menu-title'),
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
if (categoriesPath.length === 1) {
|
|
3319
|
+
// If only one category, go back to reports root
|
|
3320
|
+
return {
|
|
3321
|
+
title: await this.translateService.translateAsync('@report-management:report-runner.terms.menu-title'),
|
|
3322
|
+
};
|
|
3323
|
+
}
|
|
3324
|
+
// Go back to parent category
|
|
3325
|
+
const parentCategory = categoriesPath[categoriesPath.length - 1];
|
|
3326
|
+
return {
|
|
3327
|
+
title: parentCategory.title,
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
async onBackButtonClick() {
|
|
3331
|
+
// On mobile, if report has been executed (showing report), go back to filters
|
|
3332
|
+
if (this.deviceService.isSmall() && this.vm.hasBeenExecuted()) {
|
|
3333
|
+
this.vm.clearExecutionState();
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
const categoriesPath = this.vm.categoriesPath();
|
|
3337
|
+
if (!categoriesPath || categoriesPath.length <= 1) {
|
|
3338
|
+
// Navigate back to reports root
|
|
3339
|
+
await this.router.navigate([...this.baseRoutes, 'reports']);
|
|
3340
|
+
}
|
|
3341
|
+
else {
|
|
3342
|
+
// Navigate to parent category
|
|
3343
|
+
let urlParts = [...this.baseRoutes, 'reports'];
|
|
3344
|
+
for (const cat of categoriesPath) {
|
|
3345
|
+
urlParts.push(cat.id);
|
|
3346
|
+
}
|
|
3347
|
+
await this.router.navigate(urlParts);
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
async getPrimaryMenuItems() {
|
|
3351
|
+
const currentLayout = this.vm.currentLayout();
|
|
3352
|
+
if (!currentLayout) {
|
|
3353
|
+
return [];
|
|
3354
|
+
}
|
|
3355
|
+
// Get primary actions from service (includes general actions + layout-specific actions)
|
|
3356
|
+
const primaryActions = this.reportViewerService.getPrimaryActions(currentLayout);
|
|
3357
|
+
// Convert AXPReportLayoutAction[] to AXPActionMenuItem[] (remove priority property)
|
|
3358
|
+
return primaryActions.map(({ priority, ...action }) => action);
|
|
3359
|
+
}
|
|
3360
|
+
async getSecondaryMenuItems() {
|
|
3361
|
+
const currentLayout = this.vm.currentLayout();
|
|
3362
|
+
if (!currentLayout) {
|
|
3363
|
+
return [];
|
|
3364
|
+
}
|
|
3365
|
+
// Get secondary actions from service (includes general actions + layout-specific actions)
|
|
3366
|
+
const secondaryActions = this.reportViewerService.getSecondaryActions(currentLayout);
|
|
3367
|
+
// Convert AXPReportLayoutAction[] to AXPActionMenuItem[] (remove priority property)
|
|
3368
|
+
return secondaryActions.map(({ priority, ...action }) => action);
|
|
3369
|
+
}
|
|
3370
|
+
async execute(command) {
|
|
3371
|
+
// Handle layout switching from title actions
|
|
3372
|
+
if (command.metadata?.['isLayout']) {
|
|
3373
|
+
const layoutId = command.metadata?.['layoutId'];
|
|
3374
|
+
if (layoutId) {
|
|
3375
|
+
this.vm.switchLayout(layoutId);
|
|
3376
|
+
this.recompute();
|
|
3377
|
+
}
|
|
3378
|
+
return;
|
|
3379
|
+
}
|
|
3380
|
+
switch (command.name) {
|
|
3381
|
+
case 'execute-report':
|
|
3382
|
+
await this.handleExecuteReport();
|
|
3383
|
+
break;
|
|
3384
|
+
case 'refresh-report':
|
|
3385
|
+
this.handleRefresh();
|
|
3386
|
+
break;
|
|
3387
|
+
case 'reset-filters':
|
|
3388
|
+
this.handleClearFilters();
|
|
3389
|
+
break;
|
|
3390
|
+
case 'download-report':
|
|
3391
|
+
this.handleDownloadReport();
|
|
3392
|
+
break;
|
|
3393
|
+
case 'print-report':
|
|
3394
|
+
this.handlePrintReport();
|
|
3395
|
+
break;
|
|
3396
|
+
case 'export-report':
|
|
3397
|
+
await this.handleExportReport(command.options?.['format']);
|
|
3398
|
+
break;
|
|
3399
|
+
case 'navigate-to-reports':
|
|
3400
|
+
await this.router.navigate([...this.baseRoutes, 'reports']);
|
|
3401
|
+
break;
|
|
3402
|
+
case 'navigate-to-category':
|
|
3403
|
+
const path = command.options?.['path'];
|
|
3404
|
+
if (path) {
|
|
3405
|
+
await this.router.navigate(path);
|
|
3406
|
+
}
|
|
3407
|
+
break;
|
|
3408
|
+
default:
|
|
3409
|
+
const warning = await this.translateService.translateAsync('@report-management:report-viewer.messages.warnings.unknown-command', {
|
|
3410
|
+
params: { commandName: command.name },
|
|
3411
|
+
});
|
|
3412
|
+
console.warn(warning);
|
|
3413
|
+
break;
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportViewerPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3417
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPReportViewerPageComponent, isStandalone: true, selector: "axp-report-viewer-page", inputs: { reportId: { classPropertyName: "reportId", publicName: "reportId", isSignal: true, isRequired: false, transformFunction: null }, reportParameters: { classPropertyName: "reportParameters", publicName: "reportParameters", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "axp-report-viewer-page" }, providers: [
|
|
3418
|
+
AXUnsubscriber,
|
|
3419
|
+
AXPReportViewerViewModel,
|
|
3420
|
+
{
|
|
3421
|
+
provide: AXPPageLayoutBase,
|
|
3422
|
+
useExisting: AXPReportViewerPageComponent,
|
|
3423
|
+
},
|
|
3424
|
+
], viewQueries: [{ propertyName: "filtersForm", first: true, predicate: ["filtersForm"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<axp-page-layout *translate=\"let t\">\n <!-- Left Panel: Filters (Always visible for desktop) -->\n @if (showFiltersPanel()) {\n <axp-layout-start-side>\n <axp-layout-header>\n <axp-layout-title>\n {{ t('@report-management:report-viewer.components.filters.title') | async }}\n </axp-layout-title>\n </axp-layout-header>\n <axp-layout-content>\n @if (vm.reportDefinition()) {\n <axp-layout-renderer #filtersForm [layout]=\"vm.formDefinition()\" [context]=\"vm.filterContext()\" [look]=\"'card'\"\n (contextChange)=\"handleFiltersChange($event)\" (contextInitiated)=\"handleFiltersInitiated($event)\">\n </axp-layout-renderer>\n }\n </axp-layout-content>\n <axp-layout-footer class=\"p-4 border-t\" *translate=\"let t\">\n @if (vm.hasUnappliedFilters()) {\n <div class=\"w-full warning-lightest-surface mb-2 p-2 rounded border border-warning-200\">\n <div class=\"flex items-center gap-2 text-warning-800\">\n <ax-icon class=\"fa-light fa-exclamation-triangle text-sm\"></ax-icon>\n <span class=\"text-xs\">{{\n t('@report-management:report-viewer.components.filters.messages.warning.unapplied') | async\n }}</span>\n </div>\n </div>\n }\n <ax-button class=\"w-full\" [disabled]=\"!vm.canExecute()\"\n [text]=\"t('@report-management:report-viewer.actions.execute.title') | async\" [color]=\"'primary'\"\n (onClick)=\"execute({ name: 'execute-report' })\">\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-play\"></ax-icon>\n </ax-prefix>\n </ax-button>\n </axp-layout-footer>\n </axp-layout-start-side>\n }\n <!-- Main Content: Report Display -->\n <axp-page-content class=\"overflow-auto\">\n @if (vm.hasError()) {\n <!-- Error State -->\n <axp-state-message icon=\"fa-light fa-exclamation-triangle\"\n [title]=\"'@report-management:report-viewer.messages.error.fetch-title'\"\n [description]=\"vm.error() || '@report-management:report-viewer.messages.error.unexpected'\">\n <div slot=\"actions\">\n <ax-button look=\"outline\" color=\"primary\"\n [text]=\"t('@report-management:report-viewer.actions.retry.title') | async\" (onClick)=\"vm.clearError()\">\n <ax-prefix>\n <ax-icon class=\"fa-light fa-refresh\"></ax-icon>\n </ax-prefix>\n </ax-button>\n </div>\n </axp-state-message>\n } @else if (!vm.reportDefinition()) {\n <!-- No Report Selected State -->\n <axp-state-message icon=\"fa-light fa-chart-line\"\n [title]=\"'@report-management:report-viewer.components.report-viewer.empty-states.no-report.title'\"\n [description]=\"'@report-management:report-viewer.components.report-viewer.empty-states.no-report.description'\">\n <div slot=\"actions\">\n <ax-button look=\"outline\" color=\"primary\"\n [text]=\"t('@report-management:report-viewer.actions.browse-reports.title') | async\">\n <ax-prefix>\n <ax-icon class=\"fa-light fa-folder-open\"></ax-icon>\n </ax-prefix>\n </ax-button>\n </div>\n </axp-state-message>\n } @else {\n <ng-container *ngTemplateOutlet=\"reportDisplay\"></ng-container>\n }\n </axp-page-content>\n</axp-page-layout>\n\n<!-- Desktop: Report Display Area -->\n<ng-template #reportDisplay>\n @if (showReportDisplay()) {\n <!-- Desktop & Mobile: Report Display Area -->\n @if (vm.isTableLayout()) {\n <axp-report-table-view> </axp-report-table-view>\n } @else if (vm.isChartLayout()) {\n <axp-report-chart-view> </axp-report-chart-view>\n } @else if (vm.isPdfLayout()) {\n <axp-report-pdf-view> </axp-report-pdf-view>\n }\n } @else if (vm.reportDefinition()) {\n <!-- Mobile: Show filters -->\n <axp-layout-renderer #filtersForm [layout]=\"vm.formDefinition()\" [context]=\"vm.filterContext()\" [look]=\"'fieldset'\">\n </axp-layout-renderer>\n }\n</ng-template>\n\n<!-- Mobile: Execute Button -->\n@if (deviceService.isSmall() && !vm.hasBeenExecuted()) {\n<axp-page-footer class=\"p-2 border-t\">\n <ax-button class=\"w-full\" [disabled]=\"!vm.canExecute()\"\n [text]=\"'@report-management:report-viewer.actions.execute.title' | translate | async\" [color]=\"'primary'\"\n (onClick)=\"execute({ name: 'execute-report' })\">\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-play\"></ax-icon>\n </ax-prefix>\n </ax-button>\n</axp-page-footer>\n}", styles: ["@layer properties;.axp-report-viewer-page{display:flex;height:100%;width:100%;flex-direction:column}.axp-report-viewer-page axp-layout-start-side{height:100%;width:calc(var(--spacing, .25rem) * 80);border-inline-end-style:var(--tw-border-style);border-inline-end-width:1px}@media(width>=1920px){.axp-report-viewer-page axp-layout-start-side{width:calc(var(--spacing, .25rem) * 96)}}.axp-report-viewer-page axp-layout-start-side>axp-layout-header{height:73px;border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.axp-report-viewer-page axp-layout-start-side axp-layout-content{overflow-x:auto;padding:calc(var(--spacing, .25rem) * 4)}.axp-report-viewer-page .__mobile-filters{display:flex;height:100%;flex-direction:column;overflow-y:auto}.axp-report-viewer-page .__mobile-filters .__mobile-filters-content{flex:1}.axp-report-viewer-page .__mobile-filters .__mobile-filters-footer{position:sticky;bottom:0;border-top-style:var(--tw-border-style);border-top-width:1px;background-color:var(--color-lightest);color:var(--color-on-lightest);border-color:var(--color-border-lightest);padding-top:calc(var(--spacing, .25rem) * 2)}.axp-report-viewer-page axp-page-content{display:flex;height:100%;flex-direction:column}.axp-report-viewer-page axp-report-table-view{flex:1;overflow:hidden}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid}}}\n/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i2$1.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i3.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "component", type: i3.AXDecoratorGenericComponent, selector: "ax-footer, ax-header, ax-content, ax-divider, ax-form-hint, ax-prefix, ax-suffix, ax-text, ax-title, ax-subtitle, ax-placeholder, ax-overlay" }, { kind: "ngmodule", type: AXLoadingModule }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "directive", type: i1.AXTranslatorDirective, selector: "[translate]" }, { kind: "ngmodule", type: AXBadgeModule }, { kind: "ngmodule", type: AXLoadingDialogModule }, { kind: "component", type:
|
|
3425
|
+
//
|
|
3426
|
+
AXPLayoutRendererComponent, selector: "axp-layout-renderer", inputs: ["layout", "context", "look", "mode"], outputs: ["contextChange", "contextInitiated", "validityChange"] }, { kind: "component", type: AXPPageLayoutComponent, selector: "axp-page-layout" }, { kind: "component", type: AXPThemeLayoutStartSideComponent, selector: "axp-layout-page-start-side, axp-layout-start-side" }, { kind: "component", type: AXPThemeLayoutHeaderComponent, selector: "axp-layout-header" }, { kind: "component", type: AXPThemeLayoutBlockComponent, selector: " axp-page-content, axp-page-footer-container, axp-page-footer, axp-page-header, axp-page-header-container, axp-page-toolbar, axp-layout-content, axp-layout-page-content, axp-layout-sections, axp-layout-body, axp-layout-page-body, axp-layout-prefix, axp-layout-suffix, axp-layout-title-bar, axp-layout-title, axp-layout-title-actions, axp-layout-nav-button, axp-layout-description, axp-layout-breadcrumbs, axp-layout-list-action, " }, { kind: "component", type: AXPReportTableViewComponent, selector: "axp-report-table-view" }, { kind: "component", type: AXPThemeLayoutFooterComponent, selector: "axp-layout-footer, axp-layout-page-footer" }, { kind: "component", type: AXPReportChartViewComponent, selector: "axp-report-chart-view" }, { kind: "component", type: AXPReportPdfViewComponent, selector: "axp-report-pdf-view" }, { kind: "component", type: AXPStateMessageComponent, selector: "axp-state-message", inputs: ["mode", "icon", "title", "description", "titleParams", "descriptionParams", "look"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
3427
|
+
}
|
|
3428
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportViewerPageComponent, decorators: [{
|
|
3429
|
+
type: Component,
|
|
3430
|
+
args: [{ selector: 'axp-report-viewer-page', standalone: true, imports: [
|
|
3431
|
+
CommonModule,
|
|
3432
|
+
AXButtonModule,
|
|
3433
|
+
AXDecoratorModule,
|
|
3434
|
+
AXLoadingModule,
|
|
3435
|
+
AXTranslationModule,
|
|
3436
|
+
AXBadgeModule,
|
|
3437
|
+
AXLoadingDialogModule,
|
|
3438
|
+
//
|
|
3439
|
+
AXPLayoutRendererComponent,
|
|
3440
|
+
AXPPageLayoutComponent,
|
|
3441
|
+
AXPThemeLayoutStartSideComponent,
|
|
3442
|
+
AXPThemeLayoutHeaderComponent,
|
|
3443
|
+
AXPThemeLayoutBlockComponent,
|
|
3444
|
+
AXPReportTableViewComponent,
|
|
3445
|
+
AXPThemeLayoutFooterComponent,
|
|
3446
|
+
AXPReportChartViewComponent,
|
|
3447
|
+
AXPReportPdfViewComponent,
|
|
3448
|
+
AXPStateMessageComponent,
|
|
3449
|
+
], providers: [
|
|
3450
|
+
AXUnsubscriber,
|
|
3451
|
+
AXPReportViewerViewModel,
|
|
3452
|
+
{
|
|
3453
|
+
provide: AXPPageLayoutBase,
|
|
3454
|
+
useExisting: AXPReportViewerPageComponent,
|
|
3455
|
+
},
|
|
3456
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: { class: 'axp-report-viewer-page' }, template: "<axp-page-layout *translate=\"let t\">\n <!-- Left Panel: Filters (Always visible for desktop) -->\n @if (showFiltersPanel()) {\n <axp-layout-start-side>\n <axp-layout-header>\n <axp-layout-title>\n {{ t('@report-management:report-viewer.components.filters.title') | async }}\n </axp-layout-title>\n </axp-layout-header>\n <axp-layout-content>\n @if (vm.reportDefinition()) {\n <axp-layout-renderer #filtersForm [layout]=\"vm.formDefinition()\" [context]=\"vm.filterContext()\" [look]=\"'card'\"\n (contextChange)=\"handleFiltersChange($event)\" (contextInitiated)=\"handleFiltersInitiated($event)\">\n </axp-layout-renderer>\n }\n </axp-layout-content>\n <axp-layout-footer class=\"p-4 border-t\" *translate=\"let t\">\n @if (vm.hasUnappliedFilters()) {\n <div class=\"w-full warning-lightest-surface mb-2 p-2 rounded border border-warning-200\">\n <div class=\"flex items-center gap-2 text-warning-800\">\n <ax-icon class=\"fa-light fa-exclamation-triangle text-sm\"></ax-icon>\n <span class=\"text-xs\">{{\n t('@report-management:report-viewer.components.filters.messages.warning.unapplied') | async\n }}</span>\n </div>\n </div>\n }\n <ax-button class=\"w-full\" [disabled]=\"!vm.canExecute()\"\n [text]=\"t('@report-management:report-viewer.actions.execute.title') | async\" [color]=\"'primary'\"\n (onClick)=\"execute({ name: 'execute-report' })\">\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-play\"></ax-icon>\n </ax-prefix>\n </ax-button>\n </axp-layout-footer>\n </axp-layout-start-side>\n }\n <!-- Main Content: Report Display -->\n <axp-page-content class=\"overflow-auto\">\n @if (vm.hasError()) {\n <!-- Error State -->\n <axp-state-message icon=\"fa-light fa-exclamation-triangle\"\n [title]=\"'@report-management:report-viewer.messages.error.fetch-title'\"\n [description]=\"vm.error() || '@report-management:report-viewer.messages.error.unexpected'\">\n <div slot=\"actions\">\n <ax-button look=\"outline\" color=\"primary\"\n [text]=\"t('@report-management:report-viewer.actions.retry.title') | async\" (onClick)=\"vm.clearError()\">\n <ax-prefix>\n <ax-icon class=\"fa-light fa-refresh\"></ax-icon>\n </ax-prefix>\n </ax-button>\n </div>\n </axp-state-message>\n } @else if (!vm.reportDefinition()) {\n <!-- No Report Selected State -->\n <axp-state-message icon=\"fa-light fa-chart-line\"\n [title]=\"'@report-management:report-viewer.components.report-viewer.empty-states.no-report.title'\"\n [description]=\"'@report-management:report-viewer.components.report-viewer.empty-states.no-report.description'\">\n <div slot=\"actions\">\n <ax-button look=\"outline\" color=\"primary\"\n [text]=\"t('@report-management:report-viewer.actions.browse-reports.title') | async\">\n <ax-prefix>\n <ax-icon class=\"fa-light fa-folder-open\"></ax-icon>\n </ax-prefix>\n </ax-button>\n </div>\n </axp-state-message>\n } @else {\n <ng-container *ngTemplateOutlet=\"reportDisplay\"></ng-container>\n }\n </axp-page-content>\n</axp-page-layout>\n\n<!-- Desktop: Report Display Area -->\n<ng-template #reportDisplay>\n @if (showReportDisplay()) {\n <!-- Desktop & Mobile: Report Display Area -->\n @if (vm.isTableLayout()) {\n <axp-report-table-view> </axp-report-table-view>\n } @else if (vm.isChartLayout()) {\n <axp-report-chart-view> </axp-report-chart-view>\n } @else if (vm.isPdfLayout()) {\n <axp-report-pdf-view> </axp-report-pdf-view>\n }\n } @else if (vm.reportDefinition()) {\n <!-- Mobile: Show filters -->\n <axp-layout-renderer #filtersForm [layout]=\"vm.formDefinition()\" [context]=\"vm.filterContext()\" [look]=\"'fieldset'\">\n </axp-layout-renderer>\n }\n</ng-template>\n\n<!-- Mobile: Execute Button -->\n@if (deviceService.isSmall() && !vm.hasBeenExecuted()) {\n<axp-page-footer class=\"p-2 border-t\">\n <ax-button class=\"w-full\" [disabled]=\"!vm.canExecute()\"\n [text]=\"'@report-management:report-viewer.actions.execute.title' | translate | async\" [color]=\"'primary'\"\n (onClick)=\"execute({ name: 'execute-report' })\">\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-play\"></ax-icon>\n </ax-prefix>\n </ax-button>\n</axp-page-footer>\n}", styles: ["@layer properties;.axp-report-viewer-page{display:flex;height:100%;width:100%;flex-direction:column}.axp-report-viewer-page axp-layout-start-side{height:100%;width:calc(var(--spacing, .25rem) * 80);border-inline-end-style:var(--tw-border-style);border-inline-end-width:1px}@media(width>=1920px){.axp-report-viewer-page axp-layout-start-side{width:calc(var(--spacing, .25rem) * 96)}}.axp-report-viewer-page axp-layout-start-side>axp-layout-header{height:73px;border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.axp-report-viewer-page axp-layout-start-side axp-layout-content{overflow-x:auto;padding:calc(var(--spacing, .25rem) * 4)}.axp-report-viewer-page .__mobile-filters{display:flex;height:100%;flex-direction:column;overflow-y:auto}.axp-report-viewer-page .__mobile-filters .__mobile-filters-content{flex:1}.axp-report-viewer-page .__mobile-filters .__mobile-filters-footer{position:sticky;bottom:0;border-top-style:var(--tw-border-style);border-top-width:1px;background-color:var(--color-lightest);color:var(--color-on-lightest);border-color:var(--color-border-lightest);padding-top:calc(var(--spacing, .25rem) * 2)}.axp-report-viewer-page axp-page-content{display:flex;height:100%;flex-direction:column}.axp-report-viewer-page axp-report-table-view{flex:1;overflow:hidden}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid}}}\n/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */\n"] }]
|
|
3457
|
+
}], ctorParameters: () => [], propDecorators: { reportId: [{ type: i0.Input, args: [{ isSignal: true, alias: "reportId", required: false }] }], reportParameters: [{ type: i0.Input, args: [{ isSignal: true, alias: "reportParameters", required: false }] }], filtersForm: [{ type: i0.ViewChild, args: ['filtersForm', { isSignal: true }] }] } });
|
|
3458
|
+
|
|
3459
|
+
var reportViewerPage_component = /*#__PURE__*/Object.freeze({
|
|
3460
|
+
__proto__: null,
|
|
3461
|
+
AXPReportViewerPageComponent: AXPReportViewerPageComponent
|
|
3462
|
+
});
|
|
3463
|
+
|
|
3464
|
+
class AXPReportBarChartViewComponent {
|
|
3465
|
+
constructor() {
|
|
3466
|
+
//#region ---- Dependencies ----
|
|
3467
|
+
this.vm = inject(AXPReportViewerViewModel);
|
|
3468
|
+
this.AXPPageStatus = AXPPageStatus;
|
|
3469
|
+
//#endregion
|
|
3470
|
+
//#region ---- Computed Properties ----
|
|
3471
|
+
/**
|
|
3472
|
+
* Current chart layout configuration
|
|
3473
|
+
*/
|
|
3474
|
+
this.chartLayout = computed(() => {
|
|
3475
|
+
const layout = this.vm.currentLayout();
|
|
3476
|
+
return layout?.layout.type === 'chart' ? layout.layout : null;
|
|
3477
|
+
}, ...(ngDevMode ? [{ debugName: "chartLayout" }] : /* istanbul ignore next */ []));
|
|
3478
|
+
/**
|
|
3479
|
+
* Chart configuration options
|
|
3480
|
+
*/
|
|
3481
|
+
this.chartOptions = computed(() => {
|
|
3482
|
+
const chartLayout = this.chartLayout();
|
|
3483
|
+
return chartLayout?.options || null;
|
|
3484
|
+
}, ...(ngDevMode ? [{ debugName: "chartOptions" }] : /* istanbul ignore next */ []));
|
|
3485
|
+
/**
|
|
3486
|
+
* Chart type for rendering
|
|
3487
|
+
*/
|
|
3488
|
+
this.chartType = computed(() => {
|
|
3489
|
+
const options = this.chartOptions();
|
|
3490
|
+
return options?.chartType || 'bar';
|
|
3491
|
+
}, ...(ngDevMode ? [{ debugName: "chartType" }] : /* istanbul ignore next */ []));
|
|
3492
|
+
/**
|
|
3493
|
+
* Data mapping configuration
|
|
3494
|
+
*/
|
|
3495
|
+
this.dataMapping = computed(() => {
|
|
3496
|
+
const options = this.chartOptions();
|
|
3497
|
+
return options?.dataMapping || null;
|
|
3498
|
+
}, ...(ngDevMode ? [{ debugName: "dataMapping" }] : /* istanbul ignore next */ []));
|
|
3499
|
+
//#endregion
|
|
3500
|
+
//#region ---- Effects ----
|
|
3501
|
+
/**
|
|
3502
|
+
* Effect to handle data fetching when chart configuration changes
|
|
3503
|
+
*/
|
|
3504
|
+
this.#fetchData = effect(() => {
|
|
3505
|
+
if (this.vm.allowFetchData() && this.vm.shouldExecuteReport() && this.chartOptions()) {
|
|
3506
|
+
// TODO: Implement chart data fetching
|
|
3507
|
+
console.log('Chart data fetch triggered', {
|
|
3508
|
+
chartType: this.chartType(),
|
|
3509
|
+
dataMapping: this.dataMapping(),
|
|
3510
|
+
});
|
|
3511
|
+
this.vm.fetchData().then((data) => {
|
|
3512
|
+
const barChartData = AXBarChartReportMapper((data?.data).data ?? {});
|
|
3513
|
+
this.barChartData.set(barChartData);
|
|
3514
|
+
console.log('barChartData', barChartData);
|
|
3515
|
+
});
|
|
3516
|
+
}
|
|
3517
|
+
}, ...(ngDevMode ? [{ debugName: "#fetchData" }] : /* istanbul ignore next */ []));
|
|
3518
|
+
this.barChartData = signal([], ...(ngDevMode ? [{ debugName: "barChartData" }] : /* istanbul ignore next */ []));
|
|
3519
|
+
this.barChartOptions = signal({}, ...(ngDevMode ? [{ debugName: "barChartOptions" }] : /* istanbul ignore next */ []));
|
|
3520
|
+
this.legendOptions = signal({
|
|
3521
|
+
mode: 'horizontal',
|
|
3522
|
+
}, ...(ngDevMode ? [{ debugName: "legendOptions" }] : /* istanbul ignore next */ []));
|
|
3523
|
+
}
|
|
3524
|
+
//#endregion
|
|
3525
|
+
//#region ---- Effects ----
|
|
3526
|
+
/**
|
|
3527
|
+
* Effect to handle data fetching when chart configuration changes
|
|
3528
|
+
*/
|
|
3529
|
+
#fetchData;
|
|
3530
|
+
//#endregion
|
|
3531
|
+
//#region ---- Event Handlers ----
|
|
3532
|
+
/**
|
|
3533
|
+
* Handle chart click events
|
|
3534
|
+
*/
|
|
3535
|
+
handleChartClick(event) {
|
|
3536
|
+
// TODO: Implement chart click handling
|
|
3537
|
+
console.log('Chart clicked:', event);
|
|
3538
|
+
}
|
|
3539
|
+
/**
|
|
3540
|
+
* Handle chart data point selection
|
|
3541
|
+
*/
|
|
3542
|
+
handleDataPointSelect(event) {
|
|
3543
|
+
// TODO: Implement data point selection
|
|
3544
|
+
console.log('Data point selected:', event);
|
|
3545
|
+
}
|
|
3546
|
+
//#endregion
|
|
3547
|
+
//#region ---- Helper Methods ----
|
|
3548
|
+
/**
|
|
3549
|
+
* Check if chart is ready to render
|
|
3550
|
+
*/
|
|
3551
|
+
isChartReady() {
|
|
3552
|
+
return !!(this.chartOptions() && this.dataMapping());
|
|
3553
|
+
}
|
|
3554
|
+
/**
|
|
3555
|
+
* Get chart configuration for rendering
|
|
3556
|
+
*/
|
|
3557
|
+
getChartConfig() {
|
|
3558
|
+
// TODO: Transform AXPReportChartOptions to chart library format
|
|
3559
|
+
return {
|
|
3560
|
+
type: this.chartType(),
|
|
3561
|
+
options: this.chartOptions(),
|
|
3562
|
+
dataMapping: this.dataMapping(),
|
|
3563
|
+
};
|
|
3564
|
+
}
|
|
3565
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportBarChartViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3566
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPReportBarChartViewComponent, isStandalone: true, selector: "axp-report-bar-chart-view", ngImport: i0, template: "<div class=\"axp-report-chart-view\" *translate=\"let t\">\n @if (vm.status() === AXPPageStatus.Processing) {\n <!-- Loading State -->\n <div class=\"__loading\">\n <div class=\"spinner spin border-4 border-gray-200 border-t-primary rounded-full w-12 h-12\">\n </div>\n <div class=\"__loading-text\">\n {{ t('@report-management:report-viewer.components.chart.messages.loading') | async }}\n </div>\n </div>\n } @else if (vm.status() === AXPPageStatus.Error) {\n <!-- Error State -->\n <axp-state-message mode=\"error\" icon=\"fa-light fa-exclamation-triangle\"\n [title]=\"'@report-management:report-viewer.messages.error.fetch-title'\"\n [description]=\"vm.error() || '@report-management:report-viewer.messages.error.fetch-description'\">\n </axp-state-message>\n } @else if (isChartReady()) {\n <!-- Chart Content -->\n <div class=\"__chart-container\">\n @if (chartLayout()?.header) {\n <div class=\"__chart-header\">\n <h3 class=\"__chart-title\">{{ chartLayout()?.header?.title }}</h3>\n @if (chartLayout()?.header?.showDate) {\n <div class=\"__chart-date\">{{ '@general:common.date.today' | translate | async }}</div>\n }\n </div>\n }\n\n <div class=\"__chart-body\">\n <!-- Chart component will be rendered here -->\n <div class=\"__chart-placeholder\">\n <div class=\"__placeholder-content\">\n <div class=\"flex flex-wrap items-center justify-center gap-10\"\n [class]=\"legendOptions().mode === 'horizontal' ? 'flex-col' : 'flex-row'\">\n <!-- Chart Area -->\n <div class=\"flex-0 size-96\">\n <ax-bar-chart #chart [data]=\"barChartData()\" [options]=\"barChartOptions()\"></ax-bar-chart>\n </div>\n <!-- Legend Area -->\n @if (barChartData().length > 0) {\n <div>\n <ax-chart-legend [options]=\"legendOptions()\" [chart]=\"chart\"></ax-chart-legend>\n </div>\n }\n </div>\n </div>\n </div>\n </div>\n\n @if (chartLayout()?.footer) {\n <div class=\"__chart-footer\">\n @if (chartLayout()?.footer?.customHtml) {\n <div [innerHTML]=\"chartLayout()?.footer?.customHtml\"></div>\n }\n </div>\n }\n </div>\n } @else {\n <!-- No Configuration State -->\n <axp-state-message icon=\"fa-light fa-chart-column\"\n [title]=\"'@report-management:report-viewer.components.chart.empty-states.no-config.title'\"\n [description]=\"'@report-management:report-viewer.components.chart.empty-states.no-config.description'\">\n </axp-state-message>\n }\n</div>", styles: ["@layer properties;.axp-report-chart-view{display:flex;height:100%;width:100%;flex-direction:column}.axp-report-chart-view .__loading{display:flex;height:100%;flex-direction:column;align-items:center;justify-content:center;gap:calc(var(--spacing, .25rem) * 4);padding:calc(var(--spacing, .25rem) * 8)}.axp-report-chart-view .__loading .__loading-text{font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:var(--color-gray-600, oklch(44.6% .03 256.802))}.axp-report-chart-view .__chart-container{display:flex;height:100%;width:100%;flex-direction:column}.axp-report-chart-view .__chart-container .__chart-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;background-color:var(--color-gray-50, oklch(98.5% .002 247.839));padding:calc(var(--spacing, .25rem) * 4)}.axp-report-chart-view .__chart-container .__chart-header .__chart-title{margin:0;font-size:var(--text-lg, 1.125rem);line-height:var(--tw-leading, var(--text-lg--line-height, calc(1.75 / 1.125)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600)}.axp-report-chart-view .__chart-container .__chart-header .__chart-date{margin-top:var(--spacing, .25rem);font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:var(--color-gray-600, oklch(44.6% .03 256.802))}.axp-report-chart-view .__chart-container .__chart-body{flex:1;overflow:hidden}.axp-report-chart-view .__chart-container .__chart-body .__chart-placeholder{display:flex;height:100%;flex-direction:column;align-items:center;justify-content:center;gap:calc(var(--spacing, .25rem) * 6);text-align:center}.axp-report-chart-view .__chart-container .__chart-body .__chart-placeholder .__placeholder-icon{font-size:var(--text-6xl, 3.75rem);line-height:var(--tw-leading, var(--text-6xl--line-height, 1));color:rgba(var(--ax-sys-color-primary-surface))}@supports (color: color-mix(in lab,red,red)){.axp-report-chart-view .__chart-container .__chart-body .__chart-placeholder .__placeholder-icon{color:color-mix(in oklab,rgba(var(--ax-sys-color-primary-surface)) 20%,transparent)}}.axp-report-chart-view .__chart-container .__chart-body .__chart-placeholder .__placeholder-content{max-width:var(--container-md, 28rem)}.axp-report-chart-view .__chart-container .__chart-body .__chart-placeholder .__placeholder-content .__placeholder-title{margin-bottom:calc(var(--spacing, .25rem) * 2);font-size:var(--text-xl, 1.25rem);line-height:var(--tw-leading, var(--text-xl--line-height, calc(1.75 / 1.25)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600)}.axp-report-chart-view .__chart-container .__chart-body .__chart-placeholder .__placeholder-content .__placeholder-description{margin-bottom:calc(var(--spacing, .25rem) * 4);color:var(--color-gray-600, oklch(44.6% .03 256.802))}.axp-report-chart-view .__chart-container .__chart-body .__chart-content{height:100%;width:100%}.axp-report-chart-view .__chart-container .__chart-footer{border-top-style:var(--tw-border-style);border-top-width:1px;background-color:var(--color-gray-50, oklch(98.5% .002 247.839));padding:calc(var(--spacing, .25rem) * 4);font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:var(--color-gray-600, oklch(44.6% .03 256.802))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-font-weight: initial}}}\n/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "directive", type: i1.AXTranslatorDirective, selector: "[translate]" }, { kind: "component", type: AXPStateMessageComponent, selector: "axp-state-message", inputs: ["mode", "icon", "title", "description", "titleParams", "descriptionParams", "look"] }, { kind: "ngmodule", type: AXPWidgetCoreModule }, { kind: "component", type: AXBarChartComponent, selector: "ax-bar-chart", inputs: ["data", "options"], outputs: ["barClick"] }, { kind: "component", type: AXChartLegendComponent, selector: "ax-chart-legend", inputs: ["chart", "options"], outputs: ["itemClick", "itemMouseEnter", "itemMouseLeave"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
3567
|
+
}
|
|
3568
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPReportBarChartViewComponent, decorators: [{
|
|
3569
|
+
type: Component,
|
|
3570
|
+
args: [{ selector: 'axp-report-bar-chart-view', standalone: true, imports: [
|
|
3571
|
+
CommonModule,
|
|
3572
|
+
AXDecoratorModule,
|
|
3573
|
+
AXTranslationModule,
|
|
3574
|
+
AXPStateMessageComponent,
|
|
3575
|
+
AXPWidgetCoreModule,
|
|
3576
|
+
AXBarChartComponent,
|
|
3577
|
+
AXChartLegendComponent,
|
|
3578
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"axp-report-chart-view\" *translate=\"let t\">\n @if (vm.status() === AXPPageStatus.Processing) {\n <!-- Loading State -->\n <div class=\"__loading\">\n <div class=\"spinner spin border-4 border-gray-200 border-t-primary rounded-full w-12 h-12\">\n </div>\n <div class=\"__loading-text\">\n {{ t('@report-management:report-viewer.components.chart.messages.loading') | async }}\n </div>\n </div>\n } @else if (vm.status() === AXPPageStatus.Error) {\n <!-- Error State -->\n <axp-state-message mode=\"error\" icon=\"fa-light fa-exclamation-triangle\"\n [title]=\"'@report-management:report-viewer.messages.error.fetch-title'\"\n [description]=\"vm.error() || '@report-management:report-viewer.messages.error.fetch-description'\">\n </axp-state-message>\n } @else if (isChartReady()) {\n <!-- Chart Content -->\n <div class=\"__chart-container\">\n @if (chartLayout()?.header) {\n <div class=\"__chart-header\">\n <h3 class=\"__chart-title\">{{ chartLayout()?.header?.title }}</h3>\n @if (chartLayout()?.header?.showDate) {\n <div class=\"__chart-date\">{{ '@general:common.date.today' | translate | async }}</div>\n }\n </div>\n }\n\n <div class=\"__chart-body\">\n <!-- Chart component will be rendered here -->\n <div class=\"__chart-placeholder\">\n <div class=\"__placeholder-content\">\n <div class=\"flex flex-wrap items-center justify-center gap-10\"\n [class]=\"legendOptions().mode === 'horizontal' ? 'flex-col' : 'flex-row'\">\n <!-- Chart Area -->\n <div class=\"flex-0 size-96\">\n <ax-bar-chart #chart [data]=\"barChartData()\" [options]=\"barChartOptions()\"></ax-bar-chart>\n </div>\n <!-- Legend Area -->\n @if (barChartData().length > 0) {\n <div>\n <ax-chart-legend [options]=\"legendOptions()\" [chart]=\"chart\"></ax-chart-legend>\n </div>\n }\n </div>\n </div>\n </div>\n </div>\n\n @if (chartLayout()?.footer) {\n <div class=\"__chart-footer\">\n @if (chartLayout()?.footer?.customHtml) {\n <div [innerHTML]=\"chartLayout()?.footer?.customHtml\"></div>\n }\n </div>\n }\n </div>\n } @else {\n <!-- No Configuration State -->\n <axp-state-message icon=\"fa-light fa-chart-column\"\n [title]=\"'@report-management:report-viewer.components.chart.empty-states.no-config.title'\"\n [description]=\"'@report-management:report-viewer.components.chart.empty-states.no-config.description'\">\n </axp-state-message>\n }\n</div>", styles: ["@layer properties;.axp-report-chart-view{display:flex;height:100%;width:100%;flex-direction:column}.axp-report-chart-view .__loading{display:flex;height:100%;flex-direction:column;align-items:center;justify-content:center;gap:calc(var(--spacing, .25rem) * 4);padding:calc(var(--spacing, .25rem) * 8)}.axp-report-chart-view .__loading .__loading-text{font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:var(--color-gray-600, oklch(44.6% .03 256.802))}.axp-report-chart-view .__chart-container{display:flex;height:100%;width:100%;flex-direction:column}.axp-report-chart-view .__chart-container .__chart-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;background-color:var(--color-gray-50, oklch(98.5% .002 247.839));padding:calc(var(--spacing, .25rem) * 4)}.axp-report-chart-view .__chart-container .__chart-header .__chart-title{margin:0;font-size:var(--text-lg, 1.125rem);line-height:var(--tw-leading, var(--text-lg--line-height, calc(1.75 / 1.125)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600)}.axp-report-chart-view .__chart-container .__chart-header .__chart-date{margin-top:var(--spacing, .25rem);font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:var(--color-gray-600, oklch(44.6% .03 256.802))}.axp-report-chart-view .__chart-container .__chart-body{flex:1;overflow:hidden}.axp-report-chart-view .__chart-container .__chart-body .__chart-placeholder{display:flex;height:100%;flex-direction:column;align-items:center;justify-content:center;gap:calc(var(--spacing, .25rem) * 6);text-align:center}.axp-report-chart-view .__chart-container .__chart-body .__chart-placeholder .__placeholder-icon{font-size:var(--text-6xl, 3.75rem);line-height:var(--tw-leading, var(--text-6xl--line-height, 1));color:rgba(var(--ax-sys-color-primary-surface))}@supports (color: color-mix(in lab,red,red)){.axp-report-chart-view .__chart-container .__chart-body .__chart-placeholder .__placeholder-icon{color:color-mix(in oklab,rgba(var(--ax-sys-color-primary-surface)) 20%,transparent)}}.axp-report-chart-view .__chart-container .__chart-body .__chart-placeholder .__placeholder-content{max-width:var(--container-md, 28rem)}.axp-report-chart-view .__chart-container .__chart-body .__chart-placeholder .__placeholder-content .__placeholder-title{margin-bottom:calc(var(--spacing, .25rem) * 2);font-size:var(--text-xl, 1.25rem);line-height:var(--tw-leading, var(--text-xl--line-height, calc(1.75 / 1.25)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600)}.axp-report-chart-view .__chart-container .__chart-body .__chart-placeholder .__placeholder-content .__placeholder-description{margin-bottom:calc(var(--spacing, .25rem) * 4);color:var(--color-gray-600, oklch(44.6% .03 256.802))}.axp-report-chart-view .__chart-container .__chart-body .__chart-content{height:100%;width:100%}.axp-report-chart-view .__chart-container .__chart-footer{border-top-style:var(--tw-border-style);border-top-width:1px;background-color:var(--color-gray-50, oklch(98.5% .002 247.839));padding:calc(var(--spacing, .25rem) * 4);font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:var(--color-gray-600, oklch(44.6% .03 256.802))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-font-weight: initial}}}\n/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */\n"] }]
|
|
3579
|
+
}] });
|
|
3580
|
+
|
|
3581
|
+
/**
|
|
3582
|
+
* Generated bundle index. Do not edit.
|
|
3583
|
+
*/
|
|
3584
|
+
|
|
3585
|
+
export { AXBarChartReportMapper, AXLineChartReportMapper, AXMPermissionDefinitionProvider, AXMReportManagementModule, AXPDrillToReportCommand, AXPReportBarChartViewComponent, AXPReportTableViewComponent, AXPReportViewerPageComponent, AXPReportViewerService, AXPReportViewerViewModel, AXPieChartReportMapper, convertContextDataToFilterClause };
|
|
3586
|
+
//# sourceMappingURL=acorex-platform-module-report-management-client.mjs.map
|