@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,120 @@
1
+ import { buildChartOfAccountsPath } from '../accounting/navigation'
2
+ import { buildCatalogFullPageUrl } from '../editor/catalogNavigation'
3
+ import { buildDocumentFullPageUrl } from '../editor/documentNavigation'
4
+ import type { Awaitable, LookupSource } from '../metadata/types'
5
+ import { withBackTarget } from '../router/backNavigation'
6
+ import { isGuidString } from '../utils/guid'
7
+ import type { ReportLookupStoreApi } from './lookupFilters'
8
+ import { appendSourceTrail, buildReportPageUrl, type ReportRouteContext, type ReportSourceTrail } from './navigation'
9
+ import type { ReportCellActionDto } from './types'
10
+
11
+ export type ReportLookupTargetArgs = {
12
+ hint: LookupSource | null
13
+ value: unknown
14
+ routeFullPath: string
15
+ }
16
+
17
+ export type ReportCellActionNavigationOptions = {
18
+ currentReportContext?: ReportRouteContext | null
19
+ sourceTrail?: ReportSourceTrail | null
20
+ backTarget?: string | null
21
+ }
22
+
23
+ export type ReportingFrameworkConfig = {
24
+ useLookupStore: () => ReportLookupStoreApi
25
+ resolveLookupTarget?: (args: ReportLookupTargetArgs) => Awaitable<string | null>
26
+ resolveCellActionUrl?: (
27
+ action: ReportCellActionDto | null | undefined,
28
+ options?: ReportCellActionNavigationOptions,
29
+ ) => string | null
30
+ }
31
+
32
+ let configuredReporting: ReportingFrameworkConfig | null = null
33
+
34
+ function buildOpenReportActionUrl(
35
+ action: ReportCellActionDto,
36
+ options?: ReportCellActionNavigationOptions,
37
+ ): string | null {
38
+ const reportCode = String(action.report?.reportCode ?? '').trim()
39
+ if (!reportCode) return null
40
+
41
+ return buildReportPageUrl(reportCode, {
42
+ context: {
43
+ reportCode,
44
+ request: {
45
+ parameters: action.report?.parameters ?? null,
46
+ filters: action.report?.filters ?? null,
47
+ layout: null,
48
+ offset: 0,
49
+ limit: 500,
50
+ cursor: null,
51
+ },
52
+ },
53
+ sourceTrail: appendSourceTrail(options?.sourceTrail ?? null, options?.currentReportContext ?? null),
54
+ backTarget: options?.backTarget ?? null,
55
+ })
56
+ }
57
+
58
+ function withOptionalBackTarget(url: string, backTarget?: string | null): string {
59
+ return withBackTarget(url, backTarget ?? null)
60
+ }
61
+
62
+ export function configureNgbReporting(config: ReportingFrameworkConfig) {
63
+ configuredReporting = config
64
+ }
65
+
66
+ export function getConfiguredNgbReporting(): ReportingFrameworkConfig {
67
+ if (!configuredReporting) {
68
+ throw new Error('NGB reporting framework is not configured. Call configureNgbReporting(...) during app bootstrap.')
69
+ }
70
+
71
+ return configuredReporting
72
+ }
73
+
74
+ export function maybeGetConfiguredNgbReporting(): ReportingFrameworkConfig | null {
75
+ return configuredReporting
76
+ }
77
+
78
+ export async function resolveReportLookupTarget(args: ReportLookupTargetArgs): Promise<string | null> {
79
+ const resolver = getConfiguredNgbReporting().resolveLookupTarget
80
+ if (!resolver) return null
81
+ return await resolver(args)
82
+ }
83
+
84
+ export function resolveReportCellActionUrl(
85
+ action: ReportCellActionDto | null | undefined,
86
+ options?: ReportCellActionNavigationOptions,
87
+ ): string | null {
88
+ const configured = maybeGetConfiguredNgbReporting()
89
+ const configuredTarget = configured?.resolveCellActionUrl?.(action, options)
90
+ if (configuredTarget) return configuredTarget
91
+ if (!action || !action.kind) return null
92
+
93
+ try {
94
+ if (action.kind === 'open_document') {
95
+ const documentType = String(action.documentType ?? '').trim()
96
+ const id = String(action.documentId ?? '').trim()
97
+ if (!documentType || !id || !isGuidString(id)) return null
98
+ return withOptionalBackTarget(buildDocumentFullPageUrl(documentType, id), options?.backTarget)
99
+ }
100
+
101
+ if (action.kind === 'open_account') {
102
+ const accountId = String(action.accountId ?? '').trim()
103
+ if (!accountId || !isGuidString(accountId)) return null
104
+ return withOptionalBackTarget(buildChartOfAccountsPath({ panel: 'edit', id: accountId }), options?.backTarget)
105
+ }
106
+
107
+ if (action.kind === 'open_catalog') {
108
+ const catalogType = String(action.catalogType ?? '').trim()
109
+ const id = String(action.catalogId ?? '').trim()
110
+ if (!catalogType || !id || !isGuidString(id)) return null
111
+ return withOptionalBackTarget(buildCatalogFullPageUrl(catalogType, id), options?.backTarget)
112
+ }
113
+
114
+ if (action.kind === 'open_report') return buildOpenReportActionUrl(action, options)
115
+ } catch {
116
+ return null
117
+ }
118
+
119
+ return null
120
+ }
@@ -0,0 +1,15 @@
1
+ import { buildLookupFieldTargetUrl } from '../lookup/navigation'
2
+ import { useLookupStore } from '../lookup/store'
3
+ import type { ReportingFrameworkConfig } from './config'
4
+
5
+ export function createDefaultNgbReportingConfig(): ReportingFrameworkConfig {
6
+ return {
7
+ useLookupStore: () => useLookupStore(),
8
+ resolveLookupTarget: async ({ hint, value, routeFullPath }) =>
9
+ await buildLookupFieldTargetUrl({
10
+ hint,
11
+ value,
12
+ route: { fullPath: routeFullPath },
13
+ }),
14
+ }
15
+ }
@@ -0,0 +1,43 @@
1
+ import {
2
+ extractLookupIds,
3
+ hydrateResolvedLookupItems,
4
+ searchResolvedLookupItems,
5
+ } from '../metadata/filtering'
6
+ import type { LookupStoreApi } from '../metadata/types'
7
+ import type {
8
+ ReportComposerDraft,
9
+ ReportComposerLookupItem,
10
+ ReportDefinitionDto,
11
+ ReportFilterValueDto,
12
+ } from './types'
13
+
14
+ export type ReportLookupStoreApi = LookupStoreApi<ReportComposerLookupItem>
15
+
16
+ export async function searchReportLookupItems(
17
+ lookupStore: ReportLookupStoreApi,
18
+ lookup: NonNullable<NonNullable<ReportDefinitionDto['filters']>[number]['lookup']>,
19
+ query: string,
20
+ ): Promise<ReportComposerLookupItem[]> {
21
+ return await searchResolvedLookupItems(lookupStore, lookup, query)
22
+ }
23
+
24
+ export async function hydrateReportLookupItemsFromFilters(
25
+ lookupStore: ReportLookupStoreApi,
26
+ definition: Pick<ReportDefinitionDto, 'filters'>,
27
+ draft: ReportComposerDraft,
28
+ filters: Record<string, ReportFilterValueDto> | null | undefined,
29
+ ): Promise<void> {
30
+ if (!filters) return
31
+
32
+ for (const field of definition.filters ?? []) {
33
+ const state = draft.filters[field.fieldCode]
34
+ const filterValue = filters[field.fieldCode]
35
+ if (!state || !field.lookup || !filterValue) continue
36
+
37
+ const ids = extractLookupIds(filterValue.value)
38
+ if (ids.length === 0) continue
39
+
40
+ state.items = await hydrateResolvedLookupItems(lookupStore, field.lookup, ids)
41
+ if (state.items.length > 0) state.raw = ''
42
+ }
43
+ }
@@ -0,0 +1,181 @@
1
+ import { withBackTarget } from '../router/backNavigation'
2
+ import { buildExecutionRequest } from './composer'
3
+ import type { ReportComposerDraft, ReportDefinitionDto, ReportExecutionRequestDto, ReportFilterValueDto } from './types'
4
+
5
+ export type ReportRouteContext = {
6
+ reportCode: string
7
+ reportName?: string | null
8
+ request: ReportExecutionRequestDto
9
+ }
10
+
11
+ export type ReportSourceTrail = {
12
+ items: ReportRouteContext[]
13
+ }
14
+
15
+ type ReportTokenPayload = {
16
+ reportCode: string
17
+ parameters?: Record<string, string> | null
18
+ filters?: Record<string, ReportFilterValueDto> | null
19
+ }
20
+
21
+ function normalizeQueryValue(value: unknown): string | null {
22
+ if (Array.isArray(value)) return normalizeQueryValue(value[0] ?? null)
23
+ const raw = String(value ?? '').trim()
24
+ return raw.length > 0 ? raw : null
25
+ }
26
+
27
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
28
+ return !!value && typeof value === 'object' && !Array.isArray(value)
29
+ }
30
+
31
+ function encodeBase64UrlJson(value: unknown): string {
32
+ const bytes = new TextEncoder().encode(JSON.stringify(value))
33
+ let binary = ''
34
+ bytes.forEach((byte) => {
35
+ binary += String.fromCharCode(byte)
36
+ })
37
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
38
+ }
39
+
40
+ function decodeBase64UrlJson<T>(value: unknown): T | null {
41
+ const raw = normalizeQueryValue(value)
42
+ if (!raw) return null
43
+
44
+ try {
45
+ const padded = raw.replace(/-/g, '+').replace(/_/g, '/') + '==='.slice((raw.length + 3) % 4)
46
+ const binary = atob(padded)
47
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0))
48
+ return JSON.parse(new TextDecoder().decode(bytes)) as T
49
+ } catch {
50
+ return null
51
+ }
52
+ }
53
+
54
+ function normalizeFilterValue(value: unknown): ReportFilterValueDto | null {
55
+ if (!isPlainObject(value) || !('value' in value)) return null
56
+ return {
57
+ value: value.value,
58
+ includeDescendants: !!value.includeDescendants,
59
+ }
60
+ }
61
+
62
+ function normalizeExecutionRequest(value: unknown): ReportExecutionRequestDto | null {
63
+ if (!isPlainObject(value)) return null
64
+
65
+ const parameters = isPlainObject(value.parameters)
66
+ ? Object.fromEntries(Object.entries(value.parameters).map(([key, entry]) => [key, String(entry ?? '').trim()]).filter(([, entry]) => entry.length > 0))
67
+ : null
68
+
69
+ const filters = isPlainObject(value.filters)
70
+ ? Object.fromEntries(Object.entries(value.filters).map(([key, entry]) => [key, normalizeFilterValue(entry)]).filter((entry): entry is [string, ReportFilterValueDto] => !!entry[1]))
71
+ : null
72
+
73
+ const layout = isPlainObject(value.layout) ? (value.layout as ReportExecutionRequestDto['layout']) : null
74
+ const variantCode = typeof value.variantCode === 'string' && value.variantCode.trim().length > 0 ? value.variantCode.trim() : null
75
+ const offset = typeof value.offset === 'number' && Number.isFinite(value.offset) ? value.offset : 0
76
+ const limit = typeof value.limit === 'number' && Number.isFinite(value.limit) ? value.limit : 500
77
+ const cursor = typeof value.cursor === 'string' && value.cursor.trim().length > 0 ? value.cursor.trim() : null
78
+
79
+ return {
80
+ layout,
81
+ parameters: parameters && Object.keys(parameters).length > 0 ? parameters : null,
82
+ filters: filters && Object.keys(filters).length > 0 ? filters : null,
83
+ variantCode,
84
+ offset,
85
+ limit,
86
+ cursor,
87
+ }
88
+ }
89
+
90
+ function normalizeRouteContext(value: unknown): ReportRouteContext | null {
91
+ if (!isPlainObject(value)) return null
92
+ const reportCode = String(value.reportCode ?? '').trim()
93
+ if (!reportCode) return null
94
+ const request = normalizeExecutionRequest(value.request)
95
+ if (!request) return null
96
+ const reportName = typeof value.reportName === 'string' && value.reportName.trim().length > 0 ? value.reportName.trim() : null
97
+ return { reportCode, reportName, request }
98
+ }
99
+
100
+ export function encodeReportRouteContextParam(value: ReportRouteContext | null | undefined): string | null {
101
+ return value ? encodeBase64UrlJson(value) : null
102
+ }
103
+
104
+ export function encodeReportSourceTrailParam(value: ReportSourceTrail | null | undefined): string | null {
105
+ return value && value.items.length > 0 ? encodeBase64UrlJson(value) : null
106
+ }
107
+
108
+ export function buildCurrentReportContext(definition: ReportDefinitionDto, draft: ReportComposerDraft): ReportRouteContext {
109
+ return {
110
+ reportCode: definition.reportCode,
111
+ reportName: definition.name,
112
+ request: buildExecutionRequest(definition, draft),
113
+ }
114
+ }
115
+
116
+ export function buildReportPageUrl(
117
+ reportCode: string,
118
+ options?: {
119
+ context?: ReportRouteContext | null
120
+ sourceTrail?: ReportSourceTrail | null
121
+ backTarget?: string | null
122
+ },
123
+ ): string {
124
+ const path = `/reports/${encodeURIComponent(reportCode)}`
125
+ const query = new URLSearchParams()
126
+ const contextParam = encodeReportRouteContextParam(options?.context)
127
+ const sourceTrailParam = encodeReportSourceTrailParam(options?.sourceTrail)
128
+ if (contextParam) query.set('ctx', contextParam)
129
+ if (sourceTrailParam) query.set('src', sourceTrailParam)
130
+ const suffix = query.toString()
131
+ const url = suffix ? `${path}?${suffix}` : path
132
+ return withBackTarget(url, options?.backTarget ?? null)
133
+ }
134
+
135
+ export function decodeReportRouteContextParam(value: unknown): ReportRouteContext | null {
136
+ return normalizeRouteContext(decodeBase64UrlJson(value))
137
+ }
138
+
139
+ export function decodeReportSourceTrailParam(value: unknown): ReportSourceTrail | null {
140
+ const decoded = decodeBase64UrlJson<{ items?: unknown }>(value)
141
+ if (!decoded || !Array.isArray(decoded.items)) return null
142
+ const items = decoded.items.map(normalizeRouteContext).filter((entry): entry is ReportRouteContext => !!entry)
143
+ return items.length > 0 ? { items } : null
144
+ }
145
+
146
+ export function decodeReportDrilldownTarget(token: string | null | undefined): ReportRouteContext | null {
147
+ const raw = String(token ?? '').trim()
148
+ if (!raw.startsWith('report:')) return null
149
+ const payload = decodeBase64UrlJson<ReportTokenPayload>(raw.slice('report:'.length))
150
+ if (!payload || typeof payload.reportCode !== 'string' || payload.reportCode.trim().length === 0) return null
151
+
152
+ return {
153
+ reportCode: payload.reportCode.trim(),
154
+ request: {
155
+ parameters: payload.parameters && Object.keys(payload.parameters).length > 0 ? payload.parameters : null,
156
+ filters: payload.filters && Object.keys(payload.filters).length > 0 ? payload.filters : null,
157
+ layout: null,
158
+ offset: 0,
159
+ limit: 500,
160
+ cursor: null,
161
+ },
162
+ }
163
+ }
164
+
165
+ export function appendSourceTrail(sourceTrail: ReportSourceTrail | null | undefined, currentContext: ReportRouteContext | null | undefined): ReportSourceTrail | null {
166
+ const items = [...(sourceTrail?.items ?? [])]
167
+ if (currentContext) items.push(currentContext)
168
+ return items.length > 0 ? { items } : null
169
+ }
170
+
171
+ export function buildBackToSourceUrl(sourceTrail: ReportSourceTrail | null | undefined, backTarget?: string | null): string | null {
172
+ const items = sourceTrail?.items ?? []
173
+ if (items.length === 0) return null
174
+ const target = items[items.length - 1]!
175
+ const priorItems = items.slice(0, -1)
176
+ return buildReportPageUrl(target.reportCode, {
177
+ context: target,
178
+ sourceTrail: priorItems.length > 0 ? { items: priorItems } : null,
179
+ backTarget: backTarget ?? null,
180
+ })
181
+ }
@@ -0,0 +1,73 @@
1
+ import { optionLabelForFilter } from '../metadata/filtering'
2
+ import { normalizeDateOnlyValue } from '../utils/dateValues'
3
+ import { slugifyVariantCode } from './composer'
4
+ import type {
5
+ ReportComposerDraft,
6
+ ReportDefinitionDto,
7
+ ReportFilterFieldDto,
8
+ ReportVariantDto,
9
+ } from './types'
10
+
11
+ export type ReportPageBadge = {
12
+ key: string
13
+ text: string
14
+ }
15
+
16
+ export function hasFilterStateValue(field: ReportFilterFieldDto, composer: ReportComposerDraft): boolean {
17
+ const state = composer.filters[field.fieldCode]
18
+ if (!state) return false
19
+ if (state.items.length > 0) return true
20
+ return state.raw.trim().length > 0
21
+ }
22
+
23
+ export function canAutoRunReport(definition: ReportDefinitionDto, composer: ReportComposerDraft): boolean {
24
+ const missingParameter = (definition.parameters ?? []).some((parameter) => parameter.isRequired && String(composer.parameters[parameter.code] ?? '').trim().length === 0)
25
+ if (missingParameter) return false
26
+ return !(definition.filters ?? []).some((field) => field.isRequired && !hasFilterStateValue(field, composer))
27
+ }
28
+
29
+ export function normalizeParameterDataType(dataType: string): string {
30
+ return dataType.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_')
31
+ }
32
+
33
+ export function isInlineDateParameterDataType(dataType: string): boolean {
34
+ const normalized = normalizeParameterDataType(dataType)
35
+ return normalized === 'date' || normalized === 'date_only' || normalized === 'date_time_utc'
36
+ }
37
+
38
+ export function parameterLabel(parameter: { code: string; label?: string | null }): string {
39
+ return String(parameter.label ?? parameter.code).trim() || String(parameter.code ?? '').trim()
40
+ }
41
+
42
+ export function normalizeReportDateValue(value: unknown): string | null {
43
+ return normalizeDateOnlyValue(String(value ?? '').trim())
44
+ }
45
+
46
+ export function normalizeCode(value: string): string {
47
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_')
48
+ }
49
+
50
+ export function tryResolveOptionLabel(field: ReportFilterFieldDto, rawValue: string): string {
51
+ const normalized = rawValue.trim()
52
+ if (normalized.length === 0) return normalized
53
+ return optionLabelForFilter(field, normalized) || normalized
54
+ }
55
+
56
+ export function chooseAvailableVariantCode(
57
+ name: string,
58
+ variants: readonly ReportVariantDto[],
59
+ exceptVariantCode?: string | null,
60
+ ): string {
61
+ const base = slugifyVariantCode(name)
62
+
63
+ let candidate = base
64
+ let suffix = 2
65
+ const except = String(exceptVariantCode ?? '').trim()
66
+
67
+ while (variants.some((variant) => variant.variantCode === candidate && variant.variantCode !== except)) {
68
+ candidate = `${base}-${suffix}`
69
+ suffix += 1
70
+ }
71
+
72
+ return candidate
73
+ }
@@ -0,0 +1,92 @@
1
+ import {
2
+ readStorageJsonOrNull,
3
+ readStorageString,
4
+ removeStorageItem,
5
+ writeStorageJson,
6
+ writeStorageString,
7
+ } from '../utils/storage'
8
+
9
+ import type { ReportExecutionResponseDto } from './types'
10
+
11
+ const EXECUTION_PREFIX = 'ngb.report.page.execution:'
12
+ const SCROLL_PREFIX = 'ngb.report.page.scroll:'
13
+
14
+ export type ReportPageExecutionSnapshot = {
15
+ response: ReportExecutionResponseDto
16
+ consumedCursors: string[]
17
+ }
18
+
19
+ function normalizeKey(key: string | null | undefined): string | null {
20
+ const normalized = String(key ?? '').trim()
21
+ return normalized.length > 0 ? normalized : null
22
+ }
23
+
24
+ function executionStorageKey(routeStateKey: string | null | undefined): string | null {
25
+ const normalized = normalizeKey(routeStateKey)
26
+ return normalized ? `${EXECUTION_PREFIX}${normalized}` : null
27
+ }
28
+
29
+ function scrollStorageKey(routeStateKey: string | null | undefined): string | null {
30
+ const normalized = normalizeKey(routeStateKey)
31
+ return normalized ? `${SCROLL_PREFIX}${normalized}` : null
32
+ }
33
+
34
+ export function saveReportPageExecutionSnapshot(
35
+ routeStateKey: string | null | undefined,
36
+ response: ReportExecutionResponseDto,
37
+ consumedCursors: string[],
38
+ ) {
39
+ const storageKey = executionStorageKey(routeStateKey ?? null)
40
+ if (!storageKey) return
41
+
42
+ void writeStorageJson('session', storageKey, {
43
+ response,
44
+ consumedCursors: Array.from(new Set((consumedCursors ?? []).map((entry) => String(entry ?? '').trim()).filter((entry) => entry.length > 0))),
45
+ } satisfies ReportPageExecutionSnapshot)
46
+ }
47
+
48
+ export function loadReportPageExecutionSnapshot(routeStateKey: string | null | undefined): ReportPageExecutionSnapshot | null {
49
+ const storageKey = executionStorageKey(routeStateKey ?? null)
50
+ if (!storageKey) return null
51
+
52
+ const snapshot = readStorageJsonOrNull<ReportPageExecutionSnapshot>('session', storageKey)
53
+ if (!snapshot?.response?.sheet) return null
54
+
55
+ return {
56
+ response: snapshot.response,
57
+ consumedCursors: Array.from(new Set((snapshot.consumedCursors ?? []).map((entry) => String(entry ?? '').trim()).filter((entry) => entry.length > 0))),
58
+ }
59
+ }
60
+
61
+ export function clearReportPageExecutionSnapshot(routeStateKey: string | null | undefined) {
62
+ const storageKey = executionStorageKey(routeStateKey ?? null)
63
+ if (!storageKey) return
64
+ removeStorageItem('session', storageKey)
65
+ }
66
+
67
+ export function saveReportPageScrollTop(routeStateKey: string | null | undefined, scrollTop: number) {
68
+ const storageKey = scrollStorageKey(routeStateKey ?? null)
69
+ if (!storageKey) return
70
+
71
+ const normalized = Number.isFinite(scrollTop) && scrollTop > 0 ? Math.floor(scrollTop) : 0
72
+ if (normalized <= 0) {
73
+ removeStorageItem('session', storageKey)
74
+ return
75
+ }
76
+
77
+ void writeStorageString('session', storageKey, String(normalized))
78
+ }
79
+
80
+ export function loadReportPageScrollTop(routeStateKey: string | null | undefined): number {
81
+ const storageKey = scrollStorageKey(routeStateKey ?? null)
82
+ if (!storageKey) return 0
83
+
84
+ const parsed = Number(readStorageString('session', storageKey) ?? '')
85
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0
86
+ }
87
+
88
+ export function clearReportPageScrollTop(routeStateKey: string | null | undefined) {
89
+ const storageKey = scrollStorageKey(routeStateKey ?? null)
90
+ if (!storageKey) return
91
+ removeStorageItem('session', storageKey)
92
+ }
@@ -0,0 +1,66 @@
1
+ import { ReportRowKind, type ReportExecutionRequestDto, type ReportExecutionResponseDto, type ReportSheetDto, type ReportSheetRowDto } from './types'
2
+ import { stableEquals } from '../utils/stableValue'
3
+
4
+ function normalizeCursor(value: string | null | undefined): string | null {
5
+ const normalized = String(value ?? '').trim()
6
+ return normalized.length > 0 ? normalized : null
7
+ }
8
+
9
+ function isGrandTotalRow(row: ReportSheetRowDto | null | undefined): boolean {
10
+ if (!row) return false
11
+ if (row.rowKind === ReportRowKind.Total) return true
12
+ return String(row.semanticRole ?? '').trim().toLowerCase() === 'grand_total'
13
+ }
14
+
15
+ export function countLoadedReportRows(sheet: ReportSheetDto | null | undefined): number {
16
+ const rows = sheet?.rows ?? []
17
+ return rows.reduce((count, row) => count + (isGrandTotalRow(row) ? 0 : 1), 0)
18
+ }
19
+
20
+ function cloneSheetWithRows(sheet: ReportSheetDto, rows: ReportSheetDto['rows']): ReportSheetDto {
21
+ return {
22
+ columns: [...(sheet.columns ?? [])],
23
+ rows,
24
+ meta: sheet.meta ?? null,
25
+ headerRows: sheet.headerRows ? [...sheet.headerRows] : null,
26
+ }
27
+ }
28
+
29
+ function areSheetsAppendCompatible(left: ReportSheetDto, right: ReportSheetDto): boolean {
30
+ return stableEquals(left.columns ?? [], right.columns ?? [])
31
+ && stableEquals(left.headerRows ?? [], right.headerRows ?? [])
32
+ }
33
+
34
+ export function canAppendReportResponse(response: ReportExecutionResponseDto | null | undefined): boolean {
35
+ return !!response?.hasMore && !!normalizeCursor(response.nextCursor)
36
+ }
37
+
38
+ export function buildAppendRequest(baseRequest: ReportExecutionRequestDto, nextCursor: string): ReportExecutionRequestDto {
39
+ return {
40
+ ...baseRequest,
41
+ offset: 0,
42
+ limit: Math.max(1, baseRequest.limit ?? 500),
43
+ cursor: normalizeCursor(nextCursor),
44
+ }
45
+ }
46
+
47
+ export function mergePagedReportResponses(
48
+ current: ReportExecutionResponseDto,
49
+ next: ReportExecutionResponseDto,
50
+ ): ReportExecutionResponseDto {
51
+ if (!areSheetsAppendCompatible(current.sheet, next.sheet)) {
52
+ throw new Error('Paged report append returned an incompatible sheet shape.')
53
+ }
54
+
55
+ const mergedRows = [...(current.sheet.rows ?? []), ...(next.sheet.rows ?? [])]
56
+
57
+ return {
58
+ sheet: cloneSheetWithRows(current.sheet, mergedRows),
59
+ offset: current.offset,
60
+ limit: current.limit,
61
+ total: next.total ?? current.total ?? mergedRows.length,
62
+ hasMore: next.hasMore,
63
+ nextCursor: normalizeCursor(next.nextCursor),
64
+ diagnostics: next.diagnostics ?? current.diagnostics,
65
+ }
66
+ }