@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.
Files changed (217) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +44 -0
  3. package/package.json +61 -0
  4. package/public/favicon.svg +6 -0
  5. package/public/silent-check-sso.html +8 -0
  6. package/src/assets/ngb_logo.svg +17 -0
  7. package/src/assets/ngb_logo_color.svg +17 -0
  8. package/src/index.ts +1023 -0
  9. package/src/ngb/README.md +27 -0
  10. package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
  11. package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
  12. package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
  13. package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
  14. package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
  15. package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
  16. package/src/ngb/accounting/api.ts +79 -0
  17. package/src/ngb/accounting/generalJournalEntry.ts +127 -0
  18. package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
  19. package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
  20. package/src/ngb/accounting/navigation.ts +83 -0
  21. package/src/ngb/accounting/periodClosing.ts +75 -0
  22. package/src/ngb/accounting/periodClosingApi.ts +60 -0
  23. package/src/ngb/accounting/periodClosingTypes.ts +73 -0
  24. package/src/ngb/accounting/types.ts +57 -0
  25. package/src/ngb/api/audit.ts +23 -0
  26. package/src/ngb/api/catalogs.ts +50 -0
  27. package/src/ngb/api/contracts.ts +285 -0
  28. package/src/ngb/api/documents.ts +170 -0
  29. package/src/ngb/api/http.ts +446 -0
  30. package/src/ngb/api/lookups.ts +14 -0
  31. package/src/ngb/api/types.ts +6 -0
  32. package/src/ngb/auth/index.ts +13 -0
  33. package/src/ngb/auth/keycloak.ts +374 -0
  34. package/src/ngb/auth/router.ts +35 -0
  35. package/src/ngb/auth/types.ts +12 -0
  36. package/src/ngb/auth/useAuthStore.ts +209 -0
  37. package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
  38. package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
  39. package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
  40. package/src/ngb/command-palette/api.ts +10 -0
  41. package/src/ngb/command-palette/config.ts +16 -0
  42. package/src/ngb/command-palette/presentation.ts +85 -0
  43. package/src/ngb/command-palette/search.ts +130 -0
  44. package/src/ngb/command-palette/staticItems.ts +243 -0
  45. package/src/ngb/command-palette/storage.ts +45 -0
  46. package/src/ngb/command-palette/store.ts +716 -0
  47. package/src/ngb/command-palette/types.ts +120 -0
  48. package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
  49. package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
  50. package/src/ngb/components/NgbConfirmDialog.vue +63 -0
  51. package/src/ngb/components/NgbDialog.vue +44 -0
  52. package/src/ngb/components/NgbDrawer.vue +156 -0
  53. package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
  54. package/src/ngb/components/NgbModalShell.vue +99 -0
  55. package/src/ngb/components/NgbToolbar.vue +14 -0
  56. package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
  57. package/src/ngb/components/forms/NgbFormRow.vue +21 -0
  58. package/src/ngb/components/forms/NgbFormSection.vue +16 -0
  59. package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
  60. package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
  61. package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
  62. package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
  63. package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
  64. package/src/ngb/components/register/registerTypes.ts +51 -0
  65. package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
  66. package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
  67. package/src/ngb/components/register/useRegisterRows.ts +209 -0
  68. package/src/ngb/components/register/useRegisterViewport.ts +56 -0
  69. package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
  70. package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
  71. package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
  72. package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
  73. package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
  74. package/src/ngb/editor/NgbEntityEditor.vue +250 -0
  75. package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
  76. package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
  77. package/src/ngb/editor/catalogNavigation.ts +13 -0
  78. package/src/ngb/editor/config.ts +252 -0
  79. package/src/ngb/editor/documentCopyDraft.ts +245 -0
  80. package/src/ngb/editor/documentEffects.ts +43 -0
  81. package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
  82. package/src/ngb/editor/documentNavigation.ts +115 -0
  83. package/src/ngb/editor/documentStatus.ts +45 -0
  84. package/src/ngb/editor/entityEditorErrors.ts +162 -0
  85. package/src/ngb/editor/entityEditorPersistence.ts +241 -0
  86. package/src/ngb/editor/extensions.ts +40 -0
  87. package/src/ngb/editor/types.ts +295 -0
  88. package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
  89. package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
  90. package/src/ngb/editor/useEditorDrawerState.ts +88 -0
  91. package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
  92. package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
  93. package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
  94. package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
  95. package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
  96. package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
  97. package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
  98. package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
  99. package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
  100. package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
  101. package/src/ngb/env/runtimeConfig.ts +25 -0
  102. package/src/ngb/lookup/config.ts +42 -0
  103. package/src/ngb/lookup/defaultConfig.ts +136 -0
  104. package/src/ngb/lookup/navigation.ts +76 -0
  105. package/src/ngb/lookup/prefetch.ts +40 -0
  106. package/src/ngb/lookup/store.ts +261 -0
  107. package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
  108. package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
  109. package/src/ngb/metadata/NgbEntityForm.vue +84 -0
  110. package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
  111. package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
  112. package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
  113. package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
  114. package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
  115. package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
  116. package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
  117. package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
  118. package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
  119. package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
  120. package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
  121. package/src/ngb/metadata/config.ts +28 -0
  122. package/src/ngb/metadata/dataTypes.ts +27 -0
  123. package/src/ngb/metadata/entityForm.ts +114 -0
  124. package/src/ngb/metadata/entityModel.ts +34 -0
  125. package/src/ngb/metadata/entityValueFormatting.ts +83 -0
  126. package/src/ngb/metadata/fieldRendererState.ts +84 -0
  127. package/src/ngb/metadata/filtering.ts +153 -0
  128. package/src/ngb/metadata/lookup.ts +25 -0
  129. package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
  130. package/src/ngb/metadata/normalization.ts +100 -0
  131. package/src/ngb/metadata/referenceHydration.ts +99 -0
  132. package/src/ngb/metadata/register.ts +135 -0
  133. package/src/ngb/metadata/routePages.ts +86 -0
  134. package/src/ngb/metadata/store.ts +35 -0
  135. package/src/ngb/metadata/types.ts +276 -0
  136. package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
  137. package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
  138. package/src/ngb/metadata/useValidationFocus.ts +49 -0
  139. package/src/ngb/primitives/NgbBadge.vue +31 -0
  140. package/src/ngb/primitives/NgbButton.vue +53 -0
  141. package/src/ngb/primitives/NgbCheckbox.vue +33 -0
  142. package/src/ngb/primitives/NgbDatePicker.vue +141 -0
  143. package/src/ngb/primitives/NgbIcon.vue +545 -0
  144. package/src/ngb/primitives/NgbInput.vue +57 -0
  145. package/src/ngb/primitives/NgbLookup.vue +264 -0
  146. package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
  147. package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
  148. package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
  149. package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
  150. package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
  151. package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
  152. package/src/ngb/primitives/NgbSelect.vue +108 -0
  153. package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
  154. package/src/ngb/primitives/NgbSwitch.vue +42 -0
  155. package/src/ngb/primitives/NgbTabs.vue +116 -0
  156. package/src/ngb/primitives/NgbToastHost.vue +49 -0
  157. package/src/ngb/primitives/NgbTooltip.vue +27 -0
  158. package/src/ngb/primitives/iconNames.ts +83 -0
  159. package/src/ngb/primitives/toast.ts +66 -0
  160. package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
  161. package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
  162. package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
  163. package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
  164. package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
  165. package/src/ngb/reporting/NgbReportPage.vue +1124 -0
  166. package/src/ngb/reporting/NgbReportSheet.vue +467 -0
  167. package/src/ngb/reporting/api.ts +35 -0
  168. package/src/ngb/reporting/composer.ts +904 -0
  169. package/src/ngb/reporting/config.ts +120 -0
  170. package/src/ngb/reporting/defaultConfig.ts +15 -0
  171. package/src/ngb/reporting/lookupFilters.ts +43 -0
  172. package/src/ngb/reporting/navigation.ts +181 -0
  173. package/src/ngb/reporting/pageHelpers.ts +73 -0
  174. package/src/ngb/reporting/pageSession.ts +92 -0
  175. package/src/ngb/reporting/paging.ts +66 -0
  176. package/src/ngb/reporting/types.ts +333 -0
  177. package/src/ngb/router/backNavigation.ts +122 -0
  178. package/src/ngb/router/queryParams.ts +129 -0
  179. package/src/ngb/router/queryState.ts +121 -0
  180. package/src/ngb/router/routeAliases.ts +66 -0
  181. package/src/ngb/router/routeParams.ts +15 -0
  182. package/src/ngb/router/shareLink.ts +39 -0
  183. package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
  184. package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
  185. package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
  186. package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
  187. package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
  188. package/src/ngb/security/NgbRolesPage.vue +125 -0
  189. package/src/ngb/security/NgbUserEditorPage.vue +615 -0
  190. package/src/ngb/security/NgbUsersPage.vue +125 -0
  191. package/src/ngb/security/api.ts +91 -0
  192. package/src/ngb/security/audit.ts +21 -0
  193. package/src/ngb/security/permissions.ts +65 -0
  194. package/src/ngb/security/types.ts +157 -0
  195. package/src/ngb/security/useAccessStore.ts +69 -0
  196. package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
  197. package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
  198. package/src/ngb/site/NgbLogo.vue +44 -0
  199. package/src/ngb/site/NgbPageHeader.vue +49 -0
  200. package/src/ngb/site/NgbSiteShell.vue +286 -0
  201. package/src/ngb/site/NgbSiteSidebar.vue +270 -0
  202. package/src/ngb/site/NgbTopBar.vue +425 -0
  203. package/src/ngb/site/NgbTrendChart.vue +224 -0
  204. package/src/ngb/site/dashboardData.ts +324 -0
  205. package/src/ngb/site/mainMenuStore.ts +49 -0
  206. package/src/ngb/site/types.ts +31 -0
  207. package/src/ngb/site/useDashboardPageState.ts +98 -0
  208. package/src/ngb/site/useTheme.ts +95 -0
  209. package/src/ngb/utils/clone.ts +43 -0
  210. package/src/ngb/utils/dateValues.ts +107 -0
  211. package/src/ngb/utils/errorMessage.ts +14 -0
  212. package/src/ngb/utils/guid.ts +21 -0
  213. package/src/ngb/utils/stableValue.ts +21 -0
  214. package/src/ngb/utils/storage.ts +135 -0
  215. package/src/styles/tailwind.css +165 -0
  216. package/src/vite/publicAssets.ts +104 -0
  217. 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
+ }