@ngbplatform/ui 1.3.0
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/LICENSE +201 -0
- package/README.md +44 -0
- package/package.json +61 -0
- package/public/favicon.svg +6 -0
- package/public/silent-check-sso.html +8 -0
- package/src/assets/ngb_logo.svg +17 -0
- package/src/assets/ngb_logo_color.svg +17 -0
- package/src/index.ts +1023 -0
- package/src/ngb/README.md +27 -0
- package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
- package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
- package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
- package/src/ngb/accounting/api.ts +79 -0
- package/src/ngb/accounting/generalJournalEntry.ts +127 -0
- package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
- package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
- package/src/ngb/accounting/navigation.ts +83 -0
- package/src/ngb/accounting/periodClosing.ts +75 -0
- package/src/ngb/accounting/periodClosingApi.ts +60 -0
- package/src/ngb/accounting/periodClosingTypes.ts +73 -0
- package/src/ngb/accounting/types.ts +57 -0
- package/src/ngb/api/audit.ts +23 -0
- package/src/ngb/api/catalogs.ts +50 -0
- package/src/ngb/api/contracts.ts +285 -0
- package/src/ngb/api/documents.ts +170 -0
- package/src/ngb/api/http.ts +446 -0
- package/src/ngb/api/lookups.ts +14 -0
- package/src/ngb/api/types.ts +6 -0
- package/src/ngb/auth/index.ts +13 -0
- package/src/ngb/auth/keycloak.ts +374 -0
- package/src/ngb/auth/router.ts +35 -0
- package/src/ngb/auth/types.ts +12 -0
- package/src/ngb/auth/useAuthStore.ts +209 -0
- package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
- package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
- package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
- package/src/ngb/command-palette/api.ts +10 -0
- package/src/ngb/command-palette/config.ts +16 -0
- package/src/ngb/command-palette/presentation.ts +85 -0
- package/src/ngb/command-palette/search.ts +130 -0
- package/src/ngb/command-palette/staticItems.ts +243 -0
- package/src/ngb/command-palette/storage.ts +45 -0
- package/src/ngb/command-palette/store.ts +716 -0
- package/src/ngb/command-palette/types.ts +120 -0
- package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
- package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
- package/src/ngb/components/NgbConfirmDialog.vue +63 -0
- package/src/ngb/components/NgbDialog.vue +44 -0
- package/src/ngb/components/NgbDrawer.vue +156 -0
- package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
- package/src/ngb/components/NgbModalShell.vue +99 -0
- package/src/ngb/components/NgbToolbar.vue +14 -0
- package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
- package/src/ngb/components/forms/NgbFormRow.vue +21 -0
- package/src/ngb/components/forms/NgbFormSection.vue +16 -0
- package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
- package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
- package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
- package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
- package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
- package/src/ngb/components/register/registerTypes.ts +51 -0
- package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
- package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
- package/src/ngb/components/register/useRegisterRows.ts +209 -0
- package/src/ngb/components/register/useRegisterViewport.ts +56 -0
- package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
- package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
- package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
- package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
- package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
- package/src/ngb/editor/NgbEntityEditor.vue +250 -0
- package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
- package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
- package/src/ngb/editor/catalogNavigation.ts +13 -0
- package/src/ngb/editor/config.ts +252 -0
- package/src/ngb/editor/documentCopyDraft.ts +245 -0
- package/src/ngb/editor/documentEffects.ts +43 -0
- package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
- package/src/ngb/editor/documentNavigation.ts +115 -0
- package/src/ngb/editor/documentStatus.ts +45 -0
- package/src/ngb/editor/entityEditorErrors.ts +162 -0
- package/src/ngb/editor/entityEditorPersistence.ts +241 -0
- package/src/ngb/editor/extensions.ts +40 -0
- package/src/ngb/editor/types.ts +295 -0
- package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
- package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
- package/src/ngb/editor/useEditorDrawerState.ts +88 -0
- package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
- package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
- package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
- package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
- package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
- package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
- package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
- package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
- package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
- package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
- package/src/ngb/env/runtimeConfig.ts +25 -0
- package/src/ngb/lookup/config.ts +42 -0
- package/src/ngb/lookup/defaultConfig.ts +136 -0
- package/src/ngb/lookup/navigation.ts +76 -0
- package/src/ngb/lookup/prefetch.ts +40 -0
- package/src/ngb/lookup/store.ts +261 -0
- package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
- package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
- package/src/ngb/metadata/NgbEntityForm.vue +84 -0
- package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
- package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
- package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
- package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
- package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
- package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
- package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
- package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
- package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
- package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
- package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
- package/src/ngb/metadata/config.ts +28 -0
- package/src/ngb/metadata/dataTypes.ts +27 -0
- package/src/ngb/metadata/entityForm.ts +114 -0
- package/src/ngb/metadata/entityModel.ts +34 -0
- package/src/ngb/metadata/entityValueFormatting.ts +83 -0
- package/src/ngb/metadata/fieldRendererState.ts +84 -0
- package/src/ngb/metadata/filtering.ts +153 -0
- package/src/ngb/metadata/lookup.ts +25 -0
- package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
- package/src/ngb/metadata/normalization.ts +100 -0
- package/src/ngb/metadata/referenceHydration.ts +99 -0
- package/src/ngb/metadata/register.ts +135 -0
- package/src/ngb/metadata/routePages.ts +86 -0
- package/src/ngb/metadata/store.ts +35 -0
- package/src/ngb/metadata/types.ts +276 -0
- package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
- package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
- package/src/ngb/metadata/useValidationFocus.ts +49 -0
- package/src/ngb/primitives/NgbBadge.vue +31 -0
- package/src/ngb/primitives/NgbButton.vue +53 -0
- package/src/ngb/primitives/NgbCheckbox.vue +33 -0
- package/src/ngb/primitives/NgbDatePicker.vue +141 -0
- package/src/ngb/primitives/NgbIcon.vue +545 -0
- package/src/ngb/primitives/NgbInput.vue +57 -0
- package/src/ngb/primitives/NgbLookup.vue +264 -0
- package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
- package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
- package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
- package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
- package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
- package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
- package/src/ngb/primitives/NgbSelect.vue +108 -0
- package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
- package/src/ngb/primitives/NgbSwitch.vue +42 -0
- package/src/ngb/primitives/NgbTabs.vue +116 -0
- package/src/ngb/primitives/NgbToastHost.vue +49 -0
- package/src/ngb/primitives/NgbTooltip.vue +27 -0
- package/src/ngb/primitives/iconNames.ts +83 -0
- package/src/ngb/primitives/toast.ts +66 -0
- package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
- package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
- package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
- package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
- package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
- package/src/ngb/reporting/NgbReportPage.vue +1124 -0
- package/src/ngb/reporting/NgbReportSheet.vue +467 -0
- package/src/ngb/reporting/api.ts +35 -0
- package/src/ngb/reporting/composer.ts +904 -0
- package/src/ngb/reporting/config.ts +120 -0
- package/src/ngb/reporting/defaultConfig.ts +15 -0
- package/src/ngb/reporting/lookupFilters.ts +43 -0
- package/src/ngb/reporting/navigation.ts +181 -0
- package/src/ngb/reporting/pageHelpers.ts +73 -0
- package/src/ngb/reporting/pageSession.ts +92 -0
- package/src/ngb/reporting/paging.ts +66 -0
- package/src/ngb/reporting/types.ts +333 -0
- package/src/ngb/router/backNavigation.ts +122 -0
- package/src/ngb/router/queryParams.ts +129 -0
- package/src/ngb/router/queryState.ts +121 -0
- package/src/ngb/router/routeAliases.ts +66 -0
- package/src/ngb/router/routeParams.ts +15 -0
- package/src/ngb/router/shareLink.ts +39 -0
- package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
- package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
- package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
- package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
- package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
- package/src/ngb/security/NgbRolesPage.vue +125 -0
- package/src/ngb/security/NgbUserEditorPage.vue +615 -0
- package/src/ngb/security/NgbUsersPage.vue +125 -0
- package/src/ngb/security/api.ts +91 -0
- package/src/ngb/security/audit.ts +21 -0
- package/src/ngb/security/permissions.ts +65 -0
- package/src/ngb/security/types.ts +157 -0
- package/src/ngb/security/useAccessStore.ts +69 -0
- package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
- package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
- package/src/ngb/site/NgbLogo.vue +44 -0
- package/src/ngb/site/NgbPageHeader.vue +49 -0
- package/src/ngb/site/NgbSiteShell.vue +286 -0
- package/src/ngb/site/NgbSiteSidebar.vue +270 -0
- package/src/ngb/site/NgbTopBar.vue +425 -0
- package/src/ngb/site/NgbTrendChart.vue +224 -0
- package/src/ngb/site/dashboardData.ts +324 -0
- package/src/ngb/site/mainMenuStore.ts +49 -0
- package/src/ngb/site/types.ts +31 -0
- package/src/ngb/site/useDashboardPageState.ts +98 -0
- package/src/ngb/site/useTheme.ts +95 -0
- package/src/ngb/utils/clone.ts +43 -0
- package/src/ngb/utils/dateValues.ts +107 -0
- package/src/ngb/utils/errorMessage.ts +14 -0
- package/src/ngb/utils/guid.ts +21 -0
- package/src/ngb/utils/stableValue.ts +21 -0
- package/src/ngb/utils/storage.ts +135 -0
- package/src/styles/tailwind.css +165 -0
- package/src/vite/publicAssets.ts +104 -0
- package/vite-public-assets.js +68 -0
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReportAggregationKind,
|
|
3
|
+
ReportSortDirection,
|
|
4
|
+
ReportTimeGrain,
|
|
5
|
+
type ReportComposerDraft,
|
|
6
|
+
type ReportComposerFilterState,
|
|
7
|
+
type ReportComposerLookupItem,
|
|
8
|
+
type ReportComposerGroupingState,
|
|
9
|
+
type ReportComposerMeasureState,
|
|
10
|
+
type ReportComposerSortState,
|
|
11
|
+
type ReportDefinitionDto,
|
|
12
|
+
type ReportFieldDto,
|
|
13
|
+
type ReportExportRequestDto,
|
|
14
|
+
type ReportFilterFieldDto,
|
|
15
|
+
type ReportMeasureDto,
|
|
16
|
+
type ReportOptionItem,
|
|
17
|
+
type ReportExecutionRequestDto,
|
|
18
|
+
type ReportFilterValueDto,
|
|
19
|
+
type ReportVariantDto,
|
|
20
|
+
} from './types'
|
|
21
|
+
|
|
22
|
+
function pad2(value: number): string {
|
|
23
|
+
return String(value).padStart(2, '0')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatDateOnly(date: Date): string {
|
|
27
|
+
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function currentMonthStart(): string {
|
|
31
|
+
const now = new Date()
|
|
32
|
+
return `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-01`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function currentDay(): string {
|
|
36
|
+
return formatDateOnly(new Date())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isRangeFromParameter(normalizedCode: string): boolean {
|
|
40
|
+
return normalizedCode === 'from_utc' || normalizedCode.endsWith('.from_utc') || normalizedCode.endsWith('_from')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isRangeToParameter(normalizedCode: string): boolean {
|
|
44
|
+
return normalizedCode === 'to_utc' || normalizedCode.endsWith('.to_utc') || normalizedCode.endsWith('_to')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isAsOfParameter(normalizedCode: string): boolean {
|
|
48
|
+
return normalizedCode === 'as_of_utc' || normalizedCode === 'asof_utc' || normalizedCode.includes('as_of')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isCurrentPeriodParameter(normalizedCode: string): boolean {
|
|
52
|
+
return normalizedCode === 'period_utc' || normalizedCode === 'period'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function defaultParameterValue(code: string): string {
|
|
56
|
+
const normalized = code.trim().toLowerCase()
|
|
57
|
+
if (isRangeFromParameter(normalized)) return currentMonthStart()
|
|
58
|
+
if (isRangeToParameter(normalized)) return currentDay()
|
|
59
|
+
if (isAsOfParameter(normalized)) return currentDay()
|
|
60
|
+
if (isCurrentPeriodParameter(normalized)) return currentMonthStart()
|
|
61
|
+
return ''
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createEmptyFilterState(includeDescendants: boolean): ReportComposerFilterState {
|
|
65
|
+
return {
|
|
66
|
+
raw: '',
|
|
67
|
+
items: [],
|
|
68
|
+
includeDescendants,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getReportComposerFilterState(
|
|
73
|
+
draft: Pick<ReportComposerDraft, 'filters'> | null | undefined,
|
|
74
|
+
field: Pick<ReportFilterFieldDto, 'fieldCode' | 'defaultIncludeDescendants'>,
|
|
75
|
+
): ReportComposerFilterState {
|
|
76
|
+
return draft?.filters[field.fieldCode] ?? createEmptyFilterState(!!field.defaultIncludeDescendants)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getSelectedReportComposerFilterItem(
|
|
80
|
+
draft: Pick<ReportComposerDraft, 'filters'> | null | undefined,
|
|
81
|
+
field: Pick<ReportFilterFieldDto, 'fieldCode' | 'defaultIncludeDescendants'>,
|
|
82
|
+
): ReportComposerLookupItem | null {
|
|
83
|
+
return getReportComposerFilterState(draft, field).items[0] ?? null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function coerceReportComposerLookupItem(value: unknown): ReportComposerLookupItem | null {
|
|
87
|
+
if (!value || typeof value !== 'object') return null
|
|
88
|
+
|
|
89
|
+
const id = String((value as { id?: unknown }).id ?? '').trim()
|
|
90
|
+
if (id.length === 0) return null
|
|
91
|
+
|
|
92
|
+
const label = String((value as { label?: unknown }).label ?? '').trim() || id
|
|
93
|
+
const meta = String((value as { meta?: unknown }).meta ?? '').trim()
|
|
94
|
+
return {
|
|
95
|
+
id,
|
|
96
|
+
label,
|
|
97
|
+
meta: meta.length > 0 ? meta : undefined,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function coerceReportComposerLookupItems(value: unknown): ReportComposerLookupItem[] {
|
|
102
|
+
if (!Array.isArray(value)) return []
|
|
103
|
+
return value
|
|
104
|
+
.map((item) => coerceReportComposerLookupItem(item))
|
|
105
|
+
.filter((item): item is ReportComposerLookupItem => item !== null)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function scalarFilterToRaw(value: unknown): string {
|
|
109
|
+
if (Array.isArray(value)) return value.map((entry) => String(entry ?? '').trim()).filter((entry) => entry.length > 0).join(', ')
|
|
110
|
+
return String(value ?? '').trim()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizeCode(value: unknown): string {
|
|
114
|
+
return String(value ?? '').trim()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeOptionalText(value: unknown): string | null {
|
|
118
|
+
const normalized = normalizeCode(value)
|
|
119
|
+
return normalized.length > 0 ? normalized : null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function datasetField(definition: ReportDefinitionDto, fieldCode: string): ReportFieldDto | undefined {
|
|
123
|
+
const normalized = normalizeCode(fieldCode)
|
|
124
|
+
if (normalized.length === 0) return undefined
|
|
125
|
+
return (definition.dataset?.fields ?? []).find((field) => field.code === normalized)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function datasetMeasure(definition: ReportDefinitionDto, measureCode: string): ReportMeasureDto | undefined {
|
|
129
|
+
const normalized = normalizeCode(measureCode)
|
|
130
|
+
if (normalized.length === 0) return undefined
|
|
131
|
+
return (definition.dataset?.measures ?? []).find((measure) => measure.code === normalized)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function measureBaseLabel(definition: ReportDefinitionDto, measureCode: string): string {
|
|
135
|
+
return datasetMeasure(definition, measureCode)?.label ?? normalizeCode(measureCode)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeGroupingState(definition: ReportDefinitionDto, groups: ReportComposerGroupingState[]): ReportComposerGroupingState[] {
|
|
139
|
+
return groups
|
|
140
|
+
.map((group) => {
|
|
141
|
+
const fieldCode = normalizeCode(group.fieldCode)
|
|
142
|
+
const field = datasetField(definition, fieldCode)
|
|
143
|
+
const supportedTimeGrains = new Set(getTimeGrainOptions(field).map((option) => option.value))
|
|
144
|
+
const timeGrain = normalizeTimeGrainValue(group.timeGrain)
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
groupKey: normalizeOptionalText(group.groupKey),
|
|
148
|
+
fieldCode,
|
|
149
|
+
timeGrain: timeGrain !== null && supportedTimeGrains.has(timeGrain) ? timeGrain : null,
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
.filter((group) => group.fieldCode.length > 0)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeMeasureLabelOverride(
|
|
156
|
+
definition: ReportDefinitionDto,
|
|
157
|
+
measureCode: string,
|
|
158
|
+
aggregation: ReportAggregationKind,
|
|
159
|
+
labelOverride: string | null | undefined,
|
|
160
|
+
): string | null {
|
|
161
|
+
const trimmed = String(labelOverride ?? '').trim()
|
|
162
|
+
if (trimmed.length === 0) return null
|
|
163
|
+
return trimmed === buildAutoMeasureLabel(definition, measureCode, aggregation) ? null : trimmed
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function normalizeMeasureState(definition: ReportDefinitionDto, measures: ReportComposerMeasureState[]): ReportComposerMeasureState[] {
|
|
167
|
+
return measures
|
|
168
|
+
.map((measure) => {
|
|
169
|
+
const measureCode = normalizeCode(measure.measureCode)
|
|
170
|
+
const aggregation = normalizeAggregationValue(measure.aggregation) ?? resolveDefaultAggregation(definition, measure.measureCode)
|
|
171
|
+
return {
|
|
172
|
+
measureCode,
|
|
173
|
+
aggregation,
|
|
174
|
+
labelOverride: normalizeMeasureLabelOverride(definition, measureCode, aggregation, measure.labelOverride),
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
.filter((measure) => measure.measureCode.length > 0)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildMeasureLayoutLabelOverride(definition: ReportDefinitionDto, measure: ReportComposerMeasureState): string | null {
|
|
181
|
+
const aggregation = normalizeAggregationValue(measure.aggregation) ?? resolveDefaultAggregation(definition, measure.measureCode)
|
|
182
|
+
const trimmed = String(measure.labelOverride ?? '').trim()
|
|
183
|
+
if (trimmed.length > 0) return trimmed
|
|
184
|
+
|
|
185
|
+
const autoLabel = buildAutoMeasureLabel(definition, measure.measureCode, aggregation)
|
|
186
|
+
return autoLabel === measureBaseLabel(definition, measure.measureCode) ? null : autoLabel
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeDetailFieldState(detailFields: string[]): string[] {
|
|
190
|
+
return detailFields
|
|
191
|
+
.map((fieldCode) => normalizeCode(fieldCode))
|
|
192
|
+
.filter((fieldCode, index, items) => fieldCode.length > 0 && items.indexOf(fieldCode) === index)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildSortStateKey(fieldCode: string, timeGrain: ReportTimeGrain | null): string {
|
|
196
|
+
return `${fieldCode}|${timeGrain ?? ''}`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildAxisSortStateKey(fieldCode: string, timeGrain: ReportTimeGrain | null, appliesToColumnAxis: boolean, groupKey: string | null): string {
|
|
200
|
+
return `${fieldCode}|${appliesToColumnAxis ? 'column' : 'row'}|${timeGrain ?? ''}|${groupKey ?? ''}`
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function collectGroupedTimeGrainsByField(groups: ReportComposerGroupingState[]): Map<string, ReportTimeGrain[]> {
|
|
204
|
+
const map = new Map<string, ReportTimeGrain[]>()
|
|
205
|
+
|
|
206
|
+
for (const group of groups) {
|
|
207
|
+
if (group.timeGrain === null) continue
|
|
208
|
+
const grains = map.get(group.fieldCode) ?? []
|
|
209
|
+
if (!grains.includes(group.timeGrain)) grains.push(group.timeGrain)
|
|
210
|
+
map.set(group.fieldCode, grains)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return map
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function findGroupingByKey(groups: ReportComposerGroupingState[], fieldCode: string, groupKey: string | null): ReportComposerGroupingState | null {
|
|
217
|
+
if (!groupKey) return null
|
|
218
|
+
return groups.find((group) => group.fieldCode === fieldCode && normalizeOptionalText(group.groupKey) === groupKey) ?? null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function resolveGroupedSortTimeGrain(
|
|
222
|
+
requestedTimeGrain: ReportTimeGrain | null,
|
|
223
|
+
groupedTimeGrains: ReportTimeGrain[],
|
|
224
|
+
hasGroupingWithoutTimeGrain: boolean,
|
|
225
|
+
): ReportTimeGrain | null | undefined {
|
|
226
|
+
if (groupedTimeGrains.length > 0) {
|
|
227
|
+
if (requestedTimeGrain !== null && groupedTimeGrains.includes(requestedTimeGrain)) return requestedTimeGrain
|
|
228
|
+
return groupedTimeGrains[0] ?? null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (hasGroupingWithoutTimeGrain) {
|
|
232
|
+
return requestedTimeGrain === null ? null : undefined
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return undefined
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function normalizeSortState(
|
|
239
|
+
definition: ReportDefinitionDto,
|
|
240
|
+
rowGroups: ReportComposerGroupingState[],
|
|
241
|
+
columnGroups: ReportComposerGroupingState[],
|
|
242
|
+
detailFields: string[],
|
|
243
|
+
sorts: ReportComposerSortState[],
|
|
244
|
+
): ReportComposerSortState[] {
|
|
245
|
+
const sortableFieldCodes = new Set(
|
|
246
|
+
(definition.dataset?.fields ?? [])
|
|
247
|
+
.filter((field) => !!field.isSortable)
|
|
248
|
+
.map((field) => field.code),
|
|
249
|
+
)
|
|
250
|
+
const rowGroupTimeGrainsByField = collectGroupedTimeGrainsByField(rowGroups)
|
|
251
|
+
const columnGroupTimeGrainsByField = collectGroupedTimeGrainsByField(columnGroups)
|
|
252
|
+
|
|
253
|
+
const detailFieldSet = new Set(detailFields)
|
|
254
|
+
const seen = new Set<string>()
|
|
255
|
+
const result: ReportComposerSortState[] = []
|
|
256
|
+
|
|
257
|
+
for (const sort of sorts) {
|
|
258
|
+
const fieldCode = normalizeCode(sort.fieldCode)
|
|
259
|
+
if (fieldCode.length === 0 || !sortableFieldCodes.has(fieldCode)) continue
|
|
260
|
+
|
|
261
|
+
const direction = normalizeSortDirectionValue(sort.direction) ?? ReportSortDirection.Asc
|
|
262
|
+
const requestedTimeGrain = normalizeTimeGrainValue(sort.timeGrain)
|
|
263
|
+
const requestedGroupKey = normalizeOptionalText(sort.groupKey)
|
|
264
|
+
const requestedColumnAxis = !!sort.appliesToColumnAxis
|
|
265
|
+
|
|
266
|
+
const explicitRowGroup = findGroupingByKey(rowGroups, fieldCode, requestedGroupKey)
|
|
267
|
+
const explicitColumnGroup = findGroupingByKey(columnGroups, fieldCode, requestedGroupKey)
|
|
268
|
+
|
|
269
|
+
if (explicitRowGroup || explicitColumnGroup) {
|
|
270
|
+
const appliesToColumnAxis = !!explicitColumnGroup
|
|
271
|
+
const effectiveTimeGrain = explicitColumnGroup?.timeGrain ?? explicitRowGroup?.timeGrain ?? null
|
|
272
|
+
const key = buildAxisSortStateKey(fieldCode, effectiveTimeGrain, appliesToColumnAxis, requestedGroupKey)
|
|
273
|
+
if (seen.has(key)) continue
|
|
274
|
+
|
|
275
|
+
result.push({
|
|
276
|
+
fieldCode,
|
|
277
|
+
groupKey: requestedGroupKey,
|
|
278
|
+
appliesToColumnAxis,
|
|
279
|
+
direction,
|
|
280
|
+
timeGrain: effectiveTimeGrain,
|
|
281
|
+
})
|
|
282
|
+
seen.add(key)
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const rowGroupedTimeGrains = rowGroupTimeGrainsByField.get(fieldCode) ?? []
|
|
287
|
+
const columnGroupedTimeGrains = columnGroupTimeGrainsByField.get(fieldCode) ?? []
|
|
288
|
+
const hasRowGrouping = rowGroups.some((group) => group.fieldCode === fieldCode)
|
|
289
|
+
const hasColumnGrouping = columnGroups.some((group) => group.fieldCode === fieldCode)
|
|
290
|
+
const hasDetailField = detailFieldSet.has(fieldCode)
|
|
291
|
+
|
|
292
|
+
let appliesToColumnAxis = requestedColumnAxis
|
|
293
|
+
let effectiveTimeGrain: ReportTimeGrain | null | undefined
|
|
294
|
+
|
|
295
|
+
if (requestedColumnAxis) {
|
|
296
|
+
effectiveTimeGrain = resolveGroupedSortTimeGrain(requestedTimeGrain, columnGroupedTimeGrains, hasColumnGrouping)
|
|
297
|
+
if (effectiveTimeGrain === undefined && !hasColumnGrouping && hasRowGrouping) {
|
|
298
|
+
appliesToColumnAxis = false
|
|
299
|
+
effectiveTimeGrain = resolveGroupedSortTimeGrain(requestedTimeGrain, rowGroupedTimeGrains, hasRowGrouping)
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
effectiveTimeGrain = resolveGroupedSortTimeGrain(requestedTimeGrain, rowGroupedTimeGrains, hasRowGrouping)
|
|
303
|
+
if (effectiveTimeGrain === undefined && !hasRowGrouping && hasColumnGrouping) {
|
|
304
|
+
appliesToColumnAxis = true
|
|
305
|
+
effectiveTimeGrain = resolveGroupedSortTimeGrain(requestedTimeGrain, columnGroupedTimeGrains, hasColumnGrouping)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (effectiveTimeGrain !== undefined) {
|
|
310
|
+
const key = buildAxisSortStateKey(fieldCode, effectiveTimeGrain, appliesToColumnAxis, null)
|
|
311
|
+
if (seen.has(key)) continue
|
|
312
|
+
|
|
313
|
+
result.push({
|
|
314
|
+
fieldCode,
|
|
315
|
+
groupKey: null,
|
|
316
|
+
appliesToColumnAxis,
|
|
317
|
+
direction,
|
|
318
|
+
timeGrain: effectiveTimeGrain,
|
|
319
|
+
})
|
|
320
|
+
seen.add(key)
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (hasDetailField) {
|
|
325
|
+
const key = buildAxisSortStateKey(fieldCode, null, false, null)
|
|
326
|
+
if (seen.has(key)) continue
|
|
327
|
+
|
|
328
|
+
result.push({
|
|
329
|
+
fieldCode,
|
|
330
|
+
groupKey: null,
|
|
331
|
+
appliesToColumnAxis: false,
|
|
332
|
+
direction,
|
|
333
|
+
timeGrain: null,
|
|
334
|
+
})
|
|
335
|
+
seen.add(key)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return result
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function buildLayoutFromDraft(definition: ReportDefinitionDto, draft: ReportComposerDraft) {
|
|
343
|
+
const rowGroups = normalizeGroupingState(definition, draft.rowGroups)
|
|
344
|
+
const columnGroups = normalizeGroupingState(definition, draft.columnGroups)
|
|
345
|
+
const measures = normalizeMeasureState(definition, draft.measures)
|
|
346
|
+
const detailFields = normalizeDetailFieldState(draft.detailFields)
|
|
347
|
+
const sorts = normalizeSortState(definition, rowGroups, columnGroups, detailFields, draft.sorts)
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
rowGroups: rowGroups.map((group) => ({
|
|
351
|
+
fieldCode: group.fieldCode,
|
|
352
|
+
groupKey: group.groupKey ?? undefined,
|
|
353
|
+
timeGrain: group.timeGrain ?? undefined,
|
|
354
|
+
})),
|
|
355
|
+
columnGroups: columnGroups.map((group) => ({
|
|
356
|
+
fieldCode: group.fieldCode,
|
|
357
|
+
groupKey: group.groupKey ?? undefined,
|
|
358
|
+
timeGrain: group.timeGrain ?? undefined,
|
|
359
|
+
})),
|
|
360
|
+
measures: measures.map((measure) => ({
|
|
361
|
+
measureCode: measure.measureCode,
|
|
362
|
+
aggregation: normalizeAggregationValue(measure.aggregation) ?? resolveDefaultAggregation(definition, measure.measureCode),
|
|
363
|
+
labelOverride: buildMeasureLayoutLabelOverride(definition, measure) ?? undefined,
|
|
364
|
+
})),
|
|
365
|
+
detailFields,
|
|
366
|
+
sorts: sorts.map((sort) => ({
|
|
367
|
+
fieldCode: sort.fieldCode,
|
|
368
|
+
direction: sort.direction,
|
|
369
|
+
appliesToColumnAxis: sort.appliesToColumnAxis || undefined,
|
|
370
|
+
groupKey: sort.groupKey ?? undefined,
|
|
371
|
+
timeGrain: sort.timeGrain ?? undefined,
|
|
372
|
+
})),
|
|
373
|
+
showDetails: !!draft.showDetails,
|
|
374
|
+
showSubtotals: !!draft.showSubtotals,
|
|
375
|
+
showSubtotalsOnSeparateRows: !!draft.showSubtotalsOnSeparateRows,
|
|
376
|
+
showGrandTotals: !!draft.showGrandTotals,
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function cloneComposerDraft(draft: ReportComposerDraft): ReportComposerDraft {
|
|
381
|
+
return {
|
|
382
|
+
parameters: { ...draft.parameters },
|
|
383
|
+
filters: Object.fromEntries(
|
|
384
|
+
Object.entries(draft.filters).map(([fieldCode, state]) => [fieldCode, {
|
|
385
|
+
raw: state.raw,
|
|
386
|
+
includeDescendants: state.includeDescendants,
|
|
387
|
+
items: state.items.map((item) => ({ ...item })),
|
|
388
|
+
}]),
|
|
389
|
+
),
|
|
390
|
+
rowGroups: draft.rowGroups.map((group) => ({ ...group })),
|
|
391
|
+
columnGroups: draft.columnGroups.map((group) => ({ ...group })),
|
|
392
|
+
measures: draft.measures.map((measure) => ({ ...measure })),
|
|
393
|
+
detailFields: [...draft.detailFields],
|
|
394
|
+
sorts: draft.sorts.map((sort) => ({ ...sort })),
|
|
395
|
+
showDetails: !!draft.showDetails,
|
|
396
|
+
showSubtotals: !!draft.showSubtotals,
|
|
397
|
+
showSubtotalsOnSeparateRows: !!draft.showSubtotalsOnSeparateRows,
|
|
398
|
+
showGrandTotals: !!draft.showGrandTotals,
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function normalizeComposerDraft(definition: ReportDefinitionDto, draft: ReportComposerDraft): ReportComposerDraft {
|
|
403
|
+
const next = cloneComposerDraft(draft)
|
|
404
|
+
const allowedParameterCodes = new Set((definition.parameters ?? []).map((parameter) => parameter.code))
|
|
405
|
+
next.parameters = Object.fromEntries(
|
|
406
|
+
Object.entries(next.parameters)
|
|
407
|
+
.filter(([code]) => allowedParameterCodes.has(code)),
|
|
408
|
+
)
|
|
409
|
+
next.rowGroups = normalizeGroupingState(definition, next.rowGroups)
|
|
410
|
+
next.columnGroups = normalizeGroupingState(definition, next.columnGroups)
|
|
411
|
+
next.measures = normalizeMeasureState(definition, next.measures)
|
|
412
|
+
next.detailFields = normalizeDetailFieldState(next.detailFields)
|
|
413
|
+
next.sorts = normalizeSortState(definition, next.rowGroups, next.columnGroups, next.detailFields, next.sorts)
|
|
414
|
+
|
|
415
|
+
if (!definition.capabilities?.allowsSubtotals) {
|
|
416
|
+
next.showSubtotals = false
|
|
417
|
+
next.showSubtotalsOnSeparateRows = false
|
|
418
|
+
} else if (!definition.capabilities?.allowsSeparateRowSubtotals) {
|
|
419
|
+
next.showSubtotalsOnSeparateRows = false
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (definition.capabilities?.allowsGrandTotals === false) {
|
|
423
|
+
next.showGrandTotals = false
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!definition.capabilities?.allowsShowDetails) {
|
|
427
|
+
next.showDetails = false
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return next
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function createComposerDraft(definition: ReportDefinitionDto): ReportComposerDraft {
|
|
434
|
+
const defaultLayout = definition.defaultLayout ?? {}
|
|
435
|
+
|
|
436
|
+
const parameters = Object.fromEntries(
|
|
437
|
+
(definition.parameters ?? []).map((parameter) => [parameter.code, parameter.defaultValue?.trim() || defaultParameterValue(parameter.code)]),
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
const filters = Object.fromEntries(
|
|
441
|
+
(definition.filters ?? []).map((field) => [field.fieldCode, createEmptyFilterState(!!field.defaultIncludeDescendants)]),
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
const rowGroups: ReportComposerGroupingState[] = (defaultLayout.rowGroups ?? [])
|
|
445
|
+
.map((group) => ({
|
|
446
|
+
groupKey: normalizeOptionalText(group.groupKey),
|
|
447
|
+
fieldCode: group.fieldCode,
|
|
448
|
+
timeGrain: normalizeTimeGrainValue(group.timeGrain),
|
|
449
|
+
}))
|
|
450
|
+
|
|
451
|
+
const columnGroups: ReportComposerGroupingState[] = (defaultLayout.columnGroups ?? [])
|
|
452
|
+
.map((group) => ({
|
|
453
|
+
groupKey: normalizeOptionalText(group.groupKey),
|
|
454
|
+
fieldCode: group.fieldCode,
|
|
455
|
+
timeGrain: normalizeTimeGrainValue(group.timeGrain),
|
|
456
|
+
}))
|
|
457
|
+
|
|
458
|
+
const measures: ReportComposerMeasureState[] = (defaultLayout.measures ?? [])
|
|
459
|
+
.map((measure) => ({
|
|
460
|
+
measureCode: measure.measureCode,
|
|
461
|
+
aggregation: normalizeAggregationValue(measure.aggregation),
|
|
462
|
+
labelOverride: measure.labelOverride ?? null,
|
|
463
|
+
}))
|
|
464
|
+
|
|
465
|
+
const detailFields = [...(defaultLayout.detailFields ?? [])]
|
|
466
|
+
|
|
467
|
+
const sorts: ReportComposerSortState[] = (defaultLayout.sorts ?? [])
|
|
468
|
+
.map((sort) => ({
|
|
469
|
+
fieldCode: sort.fieldCode,
|
|
470
|
+
groupKey: normalizeOptionalText(sort.groupKey),
|
|
471
|
+
appliesToColumnAxis: !!sort.appliesToColumnAxis,
|
|
472
|
+
direction: normalizeSortDirectionValue(sort.direction) ?? ReportSortDirection.Asc,
|
|
473
|
+
timeGrain: normalizeTimeGrainValue(sort.timeGrain),
|
|
474
|
+
}))
|
|
475
|
+
|
|
476
|
+
if (measures.length === 0) {
|
|
477
|
+
const firstMeasure = definition.dataset?.measures?.[0]
|
|
478
|
+
if (firstMeasure) {
|
|
479
|
+
measures.push({
|
|
480
|
+
measureCode: firstMeasure.code,
|
|
481
|
+
aggregation: resolveDefaultAggregation(definition, firstMeasure.code),
|
|
482
|
+
labelOverride: null,
|
|
483
|
+
})
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return normalizeComposerDraft(definition, {
|
|
488
|
+
parameters,
|
|
489
|
+
filters,
|
|
490
|
+
rowGroups,
|
|
491
|
+
columnGroups,
|
|
492
|
+
measures,
|
|
493
|
+
detailFields,
|
|
494
|
+
sorts,
|
|
495
|
+
showDetails: !!defaultLayout.showDetails,
|
|
496
|
+
showSubtotals: defaultLayout.showSubtotals !== false,
|
|
497
|
+
showSubtotalsOnSeparateRows: defaultLayout.showSubtotalsOnSeparateRows === true,
|
|
498
|
+
showGrandTotals: defaultLayout.showGrandTotals !== false,
|
|
499
|
+
})
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function applyExecutionRequestToDraft(definition: ReportDefinitionDto, request: ReportExecutionRequestDto): ReportComposerDraft {
|
|
503
|
+
const next = createComposerDraft(definition)
|
|
504
|
+
const layout = request.layout ?? null
|
|
505
|
+
|
|
506
|
+
if (request.parameters) {
|
|
507
|
+
for (const [key, value] of Object.entries(request.parameters)) {
|
|
508
|
+
next.parameters[key] = String(value ?? '').trim()
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (request.filters) {
|
|
513
|
+
for (const [fieldCode, value] of Object.entries(request.filters)) {
|
|
514
|
+
const state = next.filters[fieldCode]
|
|
515
|
+
if (!state) continue
|
|
516
|
+
state.raw = scalarFilterToRaw(value?.value)
|
|
517
|
+
state.items = []
|
|
518
|
+
state.includeDescendants = !!value?.includeDescendants
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (layout) {
|
|
523
|
+
next.rowGroups = (layout.rowGroups ?? []).map((group) => ({
|
|
524
|
+
groupKey: normalizeOptionalText(group.groupKey),
|
|
525
|
+
fieldCode: group.fieldCode,
|
|
526
|
+
timeGrain: normalizeTimeGrainValue(group.timeGrain),
|
|
527
|
+
}))
|
|
528
|
+
next.columnGroups = (layout.columnGroups ?? []).map((group) => ({
|
|
529
|
+
groupKey: normalizeOptionalText(group.groupKey),
|
|
530
|
+
fieldCode: group.fieldCode,
|
|
531
|
+
timeGrain: normalizeTimeGrainValue(group.timeGrain),
|
|
532
|
+
}))
|
|
533
|
+
next.measures = (layout.measures ?? []).map((measure) => ({
|
|
534
|
+
measureCode: measure.measureCode,
|
|
535
|
+
aggregation: normalizeAggregationValue(measure.aggregation) ?? resolveDefaultAggregation(definition, measure.measureCode),
|
|
536
|
+
labelOverride: measure.labelOverride ?? null,
|
|
537
|
+
}))
|
|
538
|
+
next.detailFields = [...(layout.detailFields ?? [])]
|
|
539
|
+
next.sorts = (layout.sorts ?? [])
|
|
540
|
+
.map((sort) => ({
|
|
541
|
+
fieldCode: sort.fieldCode,
|
|
542
|
+
groupKey: normalizeOptionalText(sort.groupKey),
|
|
543
|
+
appliesToColumnAxis: !!sort.appliesToColumnAxis,
|
|
544
|
+
direction: normalizeSortDirectionValue(sort.direction) ?? ReportSortDirection.Asc,
|
|
545
|
+
timeGrain: normalizeTimeGrainValue(sort.timeGrain),
|
|
546
|
+
}))
|
|
547
|
+
|
|
548
|
+
next.showDetails = !!layout.showDetails
|
|
549
|
+
next.showSubtotals = layout.showSubtotals !== false
|
|
550
|
+
next.showSubtotalsOnSeparateRows = layout.showSubtotalsOnSeparateRows === true
|
|
551
|
+
next.showGrandTotals = layout.showGrandTotals !== false
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (next.measures.length === 0) {
|
|
555
|
+
const firstMeasure = definition.dataset?.measures?.[0]
|
|
556
|
+
if (firstMeasure) {
|
|
557
|
+
next.measures.push({
|
|
558
|
+
measureCode: firstMeasure.code,
|
|
559
|
+
aggregation: resolveDefaultAggregation(definition, firstMeasure.code),
|
|
560
|
+
labelOverride: null,
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return normalizeComposerDraft(definition, next)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export function applyVariantToDraft(definition: ReportDefinitionDto, variant: ReportVariantDto): ReportComposerDraft {
|
|
569
|
+
return applyExecutionRequestToDraft(definition, {
|
|
570
|
+
layout: variant.layout ?? null,
|
|
571
|
+
filters: variant.filters ?? null,
|
|
572
|
+
parameters: variant.parameters ?? null,
|
|
573
|
+
variantCode: variant.variantCode,
|
|
574
|
+
offset: 0,
|
|
575
|
+
limit: 500,
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function slugifyVariantCode(name: string): string {
|
|
580
|
+
const normalized = String(name ?? '')
|
|
581
|
+
.trim()
|
|
582
|
+
.toLowerCase()
|
|
583
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
584
|
+
.replace(/^-+|-+$/g, '')
|
|
585
|
+
|
|
586
|
+
return normalized || 'variant'
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function buildVariantDto(
|
|
590
|
+
definition: ReportDefinitionDto,
|
|
591
|
+
draft: ReportComposerDraft,
|
|
592
|
+
options: {
|
|
593
|
+
variantCode: string
|
|
594
|
+
name: string
|
|
595
|
+
isDefault?: boolean
|
|
596
|
+
isShared?: boolean
|
|
597
|
+
},
|
|
598
|
+
): ReportVariantDto {
|
|
599
|
+
const request = buildExecutionRequest(definition, draft)
|
|
600
|
+
return {
|
|
601
|
+
variantCode: options.variantCode,
|
|
602
|
+
reportCode: definition.reportCode,
|
|
603
|
+
name: String(options.name ?? '').trim(),
|
|
604
|
+
layout: request.layout ?? null,
|
|
605
|
+
filters: request.filters ?? null,
|
|
606
|
+
parameters: request.parameters ?? null,
|
|
607
|
+
isDefault: !!options.isDefault,
|
|
608
|
+
isShared: options.isShared !== false,
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function normalizeTimeGrainValue(value: unknown): ReportTimeGrain | null {
|
|
613
|
+
if (typeof value === 'number') {
|
|
614
|
+
switch (value) {
|
|
615
|
+
case ReportTimeGrain.Day:
|
|
616
|
+
case ReportTimeGrain.Week:
|
|
617
|
+
case ReportTimeGrain.Month:
|
|
618
|
+
case ReportTimeGrain.Quarter:
|
|
619
|
+
case ReportTimeGrain.Year:
|
|
620
|
+
return value
|
|
621
|
+
default:
|
|
622
|
+
return null
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const text = String(value ?? '').trim().toLowerCase()
|
|
626
|
+
switch (text) {
|
|
627
|
+
case '1':
|
|
628
|
+
case 'day':
|
|
629
|
+
return ReportTimeGrain.Day
|
|
630
|
+
case '2':
|
|
631
|
+
case 'week':
|
|
632
|
+
return ReportTimeGrain.Week
|
|
633
|
+
case '3':
|
|
634
|
+
case 'month':
|
|
635
|
+
return ReportTimeGrain.Month
|
|
636
|
+
case '4':
|
|
637
|
+
case 'quarter':
|
|
638
|
+
return ReportTimeGrain.Quarter
|
|
639
|
+
case '5':
|
|
640
|
+
case 'year':
|
|
641
|
+
return ReportTimeGrain.Year
|
|
642
|
+
default:
|
|
643
|
+
return null
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function normalizeSortDirectionValue(value: unknown): ReportSortDirection | null {
|
|
648
|
+
if (typeof value === 'number') {
|
|
649
|
+
switch (value) {
|
|
650
|
+
case ReportSortDirection.Asc:
|
|
651
|
+
case ReportSortDirection.Desc:
|
|
652
|
+
return value
|
|
653
|
+
default:
|
|
654
|
+
return null
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const text = String(value ?? '').trim().toLowerCase()
|
|
658
|
+
switch (text) {
|
|
659
|
+
case '1':
|
|
660
|
+
case 'asc':
|
|
661
|
+
case 'ascending':
|
|
662
|
+
return ReportSortDirection.Asc
|
|
663
|
+
case '2':
|
|
664
|
+
case 'desc':
|
|
665
|
+
case 'descending':
|
|
666
|
+
return ReportSortDirection.Desc
|
|
667
|
+
default:
|
|
668
|
+
return null
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function normalizeAggregationValue(value: unknown): ReportAggregationKind | null {
|
|
673
|
+
if (typeof value === 'number') {
|
|
674
|
+
switch (value) {
|
|
675
|
+
case ReportAggregationKind.Sum:
|
|
676
|
+
case ReportAggregationKind.Min:
|
|
677
|
+
case ReportAggregationKind.Max:
|
|
678
|
+
case ReportAggregationKind.Average:
|
|
679
|
+
case ReportAggregationKind.Count:
|
|
680
|
+
case ReportAggregationKind.CountDistinct:
|
|
681
|
+
case ReportAggregationKind.First:
|
|
682
|
+
case ReportAggregationKind.Last:
|
|
683
|
+
return value
|
|
684
|
+
default:
|
|
685
|
+
return null
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
const text = String(value ?? '').trim().toLowerCase()
|
|
689
|
+
switch (text) {
|
|
690
|
+
case '1':
|
|
691
|
+
case 'sum':
|
|
692
|
+
return ReportAggregationKind.Sum
|
|
693
|
+
case '2':
|
|
694
|
+
case 'min':
|
|
695
|
+
return ReportAggregationKind.Min
|
|
696
|
+
case '3':
|
|
697
|
+
case 'max':
|
|
698
|
+
return ReportAggregationKind.Max
|
|
699
|
+
case '4':
|
|
700
|
+
case 'average':
|
|
701
|
+
case 'avg':
|
|
702
|
+
return ReportAggregationKind.Average
|
|
703
|
+
case '5':
|
|
704
|
+
case 'count':
|
|
705
|
+
return ReportAggregationKind.Count
|
|
706
|
+
case '6':
|
|
707
|
+
case 'countdistinct':
|
|
708
|
+
case 'count_distinct':
|
|
709
|
+
case 'count distinct':
|
|
710
|
+
return ReportAggregationKind.CountDistinct
|
|
711
|
+
case '7':
|
|
712
|
+
case 'first':
|
|
713
|
+
return ReportAggregationKind.First
|
|
714
|
+
case '8':
|
|
715
|
+
case 'last':
|
|
716
|
+
return ReportAggregationKind.Last
|
|
717
|
+
default:
|
|
718
|
+
return null
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export function coerceReportTimeGrain(value: unknown): ReportTimeGrain | null {
|
|
723
|
+
return normalizeTimeGrainValue(value)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export function coerceReportSortDirection(value: unknown): ReportSortDirection | null {
|
|
727
|
+
return normalizeSortDirectionValue(value)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
export function coerceReportAggregationKind(value: unknown): ReportAggregationKind | null {
|
|
731
|
+
return normalizeAggregationValue(value)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export function resolveDefaultAggregation(definition: ReportDefinitionDto, measureCode: string): ReportAggregationKind {
|
|
735
|
+
const measure = definition.dataset?.measures?.find((entry) => entry.code === measureCode)
|
|
736
|
+
const supported = (measure?.supportedAggregations ?? [])
|
|
737
|
+
.map(normalizeAggregationValue)
|
|
738
|
+
.filter((value): value is ReportAggregationKind => value !== null)
|
|
739
|
+
if (supported.length === 1) return supported[0]!
|
|
740
|
+
if (supported.includes(ReportAggregationKind.Sum)) return ReportAggregationKind.Sum
|
|
741
|
+
return supported[0] ?? ReportAggregationKind.Sum
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
export function getGroupableFields(definition: ReportDefinitionDto): ReportFieldDto[] {
|
|
745
|
+
return (definition.dataset?.fields ?? []).filter((field) => !!field.isGroupable)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export function getSortableFields(definition: ReportDefinitionDto): ReportFieldDto[] {
|
|
749
|
+
return (definition.dataset?.fields ?? []).filter((field) => !!field.isSortable)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export function getMeasureOptions(definition: ReportDefinitionDto): ReportMeasureDto[] {
|
|
753
|
+
return definition.dataset?.measures ?? []
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export function getSelectableFields(definition: ReportDefinitionDto): ReportFieldDto[] {
|
|
757
|
+
return (definition.dataset?.fields ?? []).filter((field) => !!field.isSelectable)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export function getTimeGrainOptions(field: ReportFieldDto | null | undefined): ReportOptionItem<ReportTimeGrain>[] {
|
|
761
|
+
const seen = new Set<ReportTimeGrain>()
|
|
762
|
+
const options: ReportOptionItem<ReportTimeGrain>[] = []
|
|
763
|
+
|
|
764
|
+
for (const raw of field?.supportedTimeGrains ?? []) {
|
|
765
|
+
const grain = normalizeTimeGrainValue(raw)
|
|
766
|
+
if (grain === null || seen.has(grain)) continue
|
|
767
|
+
seen.add(grain)
|
|
768
|
+
options.push({
|
|
769
|
+
value: grain,
|
|
770
|
+
label: timeGrainLabel(grain),
|
|
771
|
+
})
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return options
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export function getAggregationOptions(definition: ReportDefinitionDto, measureCode: string): ReportOptionItem<ReportAggregationKind>[] {
|
|
778
|
+
const measure = definition.dataset?.measures?.find((entry) => entry.code === measureCode)
|
|
779
|
+
const supported = (measure?.supportedAggregations ?? [])
|
|
780
|
+
.map(normalizeAggregationValue)
|
|
781
|
+
.filter((value): value is ReportAggregationKind => value !== null)
|
|
782
|
+
const seen = new Set<ReportAggregationKind>()
|
|
783
|
+
const aggregations = (supported.length > 0 ? supported : [ReportAggregationKind.Sum]).filter((value) => {
|
|
784
|
+
if (seen.has(value)) return false
|
|
785
|
+
seen.add(value)
|
|
786
|
+
return true
|
|
787
|
+
})
|
|
788
|
+
return aggregations.map((aggregation) => ({
|
|
789
|
+
value: aggregation,
|
|
790
|
+
label: aggregationLabel(aggregation),
|
|
791
|
+
}))
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export function timeGrainLabel(grain: ReportTimeGrain | null | undefined): string {
|
|
795
|
+
switch (grain) {
|
|
796
|
+
case ReportTimeGrain.Day: return 'Day'
|
|
797
|
+
case ReportTimeGrain.Week: return 'Week'
|
|
798
|
+
case ReportTimeGrain.Month: return 'Month'
|
|
799
|
+
case ReportTimeGrain.Quarter: return 'Quarter'
|
|
800
|
+
case ReportTimeGrain.Year: return 'Year'
|
|
801
|
+
default: return 'None'
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export function aggregationLabel(aggregation: ReportAggregationKind): string {
|
|
806
|
+
switch (aggregation) {
|
|
807
|
+
case ReportAggregationKind.Sum: return 'Sum'
|
|
808
|
+
case ReportAggregationKind.Min: return 'Min'
|
|
809
|
+
case ReportAggregationKind.Max: return 'Max'
|
|
810
|
+
case ReportAggregationKind.Average: return 'Average'
|
|
811
|
+
case ReportAggregationKind.Count: return 'Count'
|
|
812
|
+
case ReportAggregationKind.CountDistinct: return 'Count Distinct'
|
|
813
|
+
case ReportAggregationKind.First: return 'First'
|
|
814
|
+
case ReportAggregationKind.Last: return 'Last'
|
|
815
|
+
default: return String(aggregation)
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
export function buildAutoMeasureLabel(
|
|
821
|
+
definition: ReportDefinitionDto,
|
|
822
|
+
measureCode: string,
|
|
823
|
+
aggregation: ReportAggregationKind | null | undefined,
|
|
824
|
+
): string {
|
|
825
|
+
const normalizedMeasureCode = normalizeCode(measureCode)
|
|
826
|
+
const effectiveAggregation = normalizeAggregationValue(aggregation) ?? resolveDefaultAggregation(definition, normalizedMeasureCode)
|
|
827
|
+
const defaultAggregation = resolveDefaultAggregation(definition, normalizedMeasureCode)
|
|
828
|
+
const baseLabel = measureBaseLabel(definition, normalizedMeasureCode)
|
|
829
|
+
return effectiveAggregation === defaultAggregation
|
|
830
|
+
? baseLabel
|
|
831
|
+
: `${baseLabel} (${aggregationLabel(effectiveAggregation)})`
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export function resolveMeasureLabel(
|
|
835
|
+
definition: ReportDefinitionDto,
|
|
836
|
+
measure: Pick<ReportComposerMeasureState, 'measureCode' | 'aggregation' | 'labelOverride'>,
|
|
837
|
+
): string {
|
|
838
|
+
const trimmed = String(measure.labelOverride ?? '').trim()
|
|
839
|
+
if (trimmed.length > 0) return trimmed
|
|
840
|
+
return buildAutoMeasureLabel(definition, measure.measureCode, measure.aggregation)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
export function sortDirectionOptions(): ReportOptionItem<ReportSortDirection>[] {
|
|
844
|
+
return [
|
|
845
|
+
{ value: ReportSortDirection.Asc, label: 'Ascending' },
|
|
846
|
+
{ value: ReportSortDirection.Desc, label: 'Descending' },
|
|
847
|
+
]
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
export function buildExecutionRequest(definition: ReportDefinitionDto, draft: ReportComposerDraft): ReportExecutionRequestDto {
|
|
851
|
+
const request = buildBaseExecutionRequest(definition, draft)
|
|
852
|
+
return {
|
|
853
|
+
...request,
|
|
854
|
+
offset: 0,
|
|
855
|
+
limit: 500,
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export function buildExportRequest(definition: ReportDefinitionDto, draft: ReportComposerDraft): ReportExportRequestDto {
|
|
860
|
+
return buildBaseExecutionRequest(definition, draft)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function buildBaseExecutionRequest(definition: ReportDefinitionDto, draft: ReportComposerDraft): ReportExportRequestDto {
|
|
864
|
+
const filterEntries: Array<[string, ReportFilterValueDto]> = []
|
|
865
|
+
for (const field of definition.filters ?? []) {
|
|
866
|
+
const state = draft.filters[field.fieldCode]
|
|
867
|
+
if (!state) continue
|
|
868
|
+
|
|
869
|
+
const itemIds = state.items.map((item) => item.id).filter((value) => value.trim().length > 0)
|
|
870
|
+
const raw = state.raw.trim()
|
|
871
|
+
const scalar = field.isMulti
|
|
872
|
+
? Array.from(new Set(raw.split(',').map((part) => part.trim()).filter((part) => part.length > 0)))
|
|
873
|
+
: raw
|
|
874
|
+
|
|
875
|
+
const value = itemIds.length > 0
|
|
876
|
+
? (field.isMulti ? itemIds : itemIds[0])
|
|
877
|
+
: field.isMulti
|
|
878
|
+
? scalar
|
|
879
|
+
: scalar || null
|
|
880
|
+
|
|
881
|
+
if (value == null) continue
|
|
882
|
+
if (Array.isArray(value) && value.length === 0) continue
|
|
883
|
+
if (typeof value === 'string' && value.length === 0) continue
|
|
884
|
+
|
|
885
|
+
filterEntries.push([field.fieldCode, {
|
|
886
|
+
value,
|
|
887
|
+
includeDescendants: !!state.includeDescendants,
|
|
888
|
+
}])
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const filters = Object.fromEntries(filterEntries)
|
|
892
|
+
|
|
893
|
+
const parameters = Object.fromEntries(
|
|
894
|
+
Object.entries(draft.parameters)
|
|
895
|
+
.map(([key, value]) => [key, String(value ?? '').trim()])
|
|
896
|
+
.filter(([, value]) => value.length > 0),
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
return {
|
|
900
|
+
parameters: Object.keys(parameters).length > 0 ? parameters : null,
|
|
901
|
+
filters: Object.keys(filters).length > 0 ? filters : null,
|
|
902
|
+
layout: buildLayoutFromDraft(definition, draft),
|
|
903
|
+
}
|
|
904
|
+
}
|