@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,1124 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, ref, watch } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+ import NgbBadge from '../primitives/NgbBadge.vue'
5
+ import NgbDatePicker from '../primitives/NgbDatePicker.vue'
6
+ import NgbDialog from '../components/NgbDialog.vue'
7
+ import NgbDrawer from '../components/NgbDrawer.vue'
8
+ import NgbIcon from '../primitives/NgbIcon.vue'
9
+ import NgbInput from '../primitives/NgbInput.vue'
10
+ import NgbLookup from '../primitives/NgbLookup.vue'
11
+ import NgbSwitch from '../primitives/NgbSwitch.vue'
12
+ import NgbPageHeader from '../site/NgbPageHeader.vue'
13
+ import { splitFilterValues, summarizeFilterValues } from '../metadata/filtering'
14
+ import { currentRouteBackTarget, navigateBack, resolveBackTarget } from '../router/backNavigation'
15
+ import { setCleanRouteQuery } from '../router/queryParams'
16
+ import { useCommandPalettePageContext } from '../command-palette/useCommandPalettePageContext'
17
+ import type { CommandPaletteItemSeed } from '../command-palette/types'
18
+ import { toErrorMessage } from '../utils/errorMessage'
19
+ import { stableStringify } from '../utils/stableValue'
20
+
21
+ import DocumentDateRangeFilter from './NgbReportDateRangeFilter.vue'
22
+ import { getConfiguredNgbReporting, resolveReportLookupTarget } from './config'
23
+ import ReportComposerPanel from './NgbReportComposerPanel.vue'
24
+ import ReportSheet from './NgbReportSheet.vue'
25
+ import {
26
+ deleteReportVariant,
27
+ executeReport,
28
+ exportReportXlsx,
29
+ getReportDefinition,
30
+ getReportVariants,
31
+ saveReportVariant,
32
+ } from './api'
33
+ import {
34
+ applyExecutionRequestToDraft,
35
+ applyVariantToDraft,
36
+ buildExecutionRequest,
37
+ buildExportRequest,
38
+ buildVariantDto,
39
+ coerceReportComposerLookupItem,
40
+ cloneComposerDraft,
41
+ createComposerDraft,
42
+ getReportComposerFilterState,
43
+ getSelectedReportComposerFilterItem,
44
+ } from './composer'
45
+ import { hydrateReportLookupItemsFromFilters, searchReportLookupItems } from './lookupFilters'
46
+ import {
47
+ canAutoRunReport,
48
+ chooseAvailableVariantCode,
49
+ isInlineDateParameterDataType,
50
+ normalizeCode,
51
+ normalizeReportDateValue,
52
+ parameterLabel,
53
+ tryResolveOptionLabel,
54
+ type ReportPageBadge,
55
+ } from './pageHelpers'
56
+ import { buildAppendRequest, canAppendReportResponse, countLoadedReportRows, mergePagedReportResponses } from './paging'
57
+ import {
58
+ clearReportPageExecutionSnapshot,
59
+ clearReportPageScrollTop,
60
+ loadReportPageExecutionSnapshot,
61
+ loadReportPageScrollTop,
62
+ saveReportPageExecutionSnapshot,
63
+ saveReportPageScrollTop,
64
+ } from './pageSession'
65
+ import type {
66
+ ReportComposerDraft,
67
+ ReportComposerLookupItem,
68
+ ReportDefinitionDto,
69
+ ReportExecutionResponseDto,
70
+ ReportFilterFieldDto,
71
+ ReportVariantDto,
72
+ } from './types'
73
+ import {
74
+ buildBackToSourceUrl,
75
+ buildCurrentReportContext,
76
+ buildReportPageUrl,
77
+ decodeReportRouteContextParam,
78
+ decodeReportSourceTrailParam,
79
+ type ReportRouteContext,
80
+ type ReportSourceTrail,
81
+ encodeReportRouteContextParam,
82
+ encodeReportSourceTrailParam,
83
+ } from './navigation'
84
+
85
+ const route = useRoute()
86
+ const router = useRouter()
87
+ const lookupStore = getConfiguredNgbReporting().useLookupStore()
88
+
89
+ type ReportSheetHandle = {
90
+ restoreScrollTop: (value: number) => void
91
+ }
92
+
93
+ const loadingDefinition = ref(false)
94
+ const running = ref(false)
95
+ const loadingMore = ref(false)
96
+ const downloading = ref(false)
97
+ const savingVariant = ref(false)
98
+ const error = ref<string | null>(null)
99
+ const definition = ref<ReportDefinitionDto | null>(null)
100
+ const draft = ref<ReportComposerDraft | null>(null)
101
+ const response = ref<ReportExecutionResponseDto | null>(null)
102
+ const composerOpen = ref(false)
103
+ const lookupItemsByFilterCode = ref<Record<string, ReportComposerLookupItem[]>>({})
104
+ const variants = ref<ReportVariantDto[]>([])
105
+ const selectedVariantCode = ref('')
106
+ const activeVariantCode = ref('')
107
+ const variantDialogOpen = ref(false)
108
+ const variantDialogMode = ref<'create' | 'edit'>('create')
109
+ const variantDialogName = ref('')
110
+ const variantDialogDefault = ref(false)
111
+ const variantDialogError = ref<string | null>(null)
112
+ const deleteVariantOpen = ref(false)
113
+ const suppressedBootstrapKey = ref<string | null>(null)
114
+ const reportSheetRef = ref<ReportSheetHandle | null>(null)
115
+ const consumedAppendCursors = ref<string[]>([])
116
+ const pendingScrollRestore = ref(0)
117
+
118
+ let loadSeq = 0
119
+ let runSeq = 0
120
+
121
+ const reportCode = computed(() => decodeURIComponent(String(route.params.reportCode ?? '').trim()))
122
+ const routeBootstrapKey = computed(() => stableStringify({
123
+ reportCode: reportCode.value,
124
+ variant: route.query.variant ?? null,
125
+ ctx: route.query.ctx ?? null,
126
+ src: route.query.src ?? null,
127
+ }))
128
+ const reportPageStateKey = computed(() => routeBootstrapKey.value)
129
+ const pageTitle = computed(() => definition.value?.name || 'Report Composer')
130
+ const pageSubtitle = computed(() => definition.value?.description || 'Composable reporting shell')
131
+ const selectedVariant = computed(() => variants.value.find((variant) => variant.variantCode === selectedVariantCode.value) ?? null)
132
+ const activeVariant = computed(() => variants.value.find((variant) => variant.variantCode === activeVariantCode.value) ?? null)
133
+ const variantOptions = computed(() => [
134
+ { value: '', label: 'Definition default' },
135
+ ...variants.value.map((variant) => ({ value: variant.variantCode, label: variant.name })),
136
+ ])
137
+ const variantSummary = computed(() => {
138
+ if (!activeVariant.value) {
139
+ if (!selectedVariant.value) return 'Using the report definition default layout and filters.'
140
+ return `Current draft uses the definition default layout and filters. "${selectedVariant.value.name}" is selected but not loaded.`
141
+ }
142
+
143
+ const tags = [activeVariant.value.isShared ? 'Shared' : 'Private']
144
+ if (activeVariant.value.isDefault) tags.push('Default')
145
+
146
+ if (!selectedVariant.value && selectedVariantCode.value !== activeVariantCode.value) {
147
+ return `Current draft uses "${activeVariant.value.name}" (${tags.join(' · ')}). Definition default is selected but not loaded.`
148
+ }
149
+
150
+ if (selectedVariant.value && selectedVariantCode.value !== activeVariantCode.value) {
151
+ return `Current draft uses "${activeVariant.value.name}" (${tags.join(' · ')}). "${selectedVariant.value.name}" is selected but not loaded.`
152
+ }
153
+
154
+ return `Current draft uses "${activeVariant.value.name}" (${tags.join(' · ')}).`
155
+ })
156
+ const createVariantDisabled = computed(() => !definition.value || !draft.value)
157
+ const editVariantDisabled = computed(() => !selectedVariant.value)
158
+ const saveVariantDisabled = computed(() => !definition.value || !draft.value || !activeVariant.value || selectedVariantCode.value !== activeVariantCode.value)
159
+ const deleteVariantDisabled = computed(() => !selectedVariant.value)
160
+ const resetVariantDisabled = computed(() => !definition.value || !draft.value)
161
+ const loadVariantDisabled = computed(() => !selectedVariant.value || selectedVariantCode.value === activeVariantCode.value)
162
+ const variantDialogTitle = computed(() => variantDialogMode.value === 'create' ? 'Create variant' : 'Edit variant')
163
+ const variantDialogSubtitle = computed(() => variantDialogMode.value === 'create'
164
+ ? 'Create a reusable named setup for this composable report.'
165
+ : 'Rename the selected variant or change whether it opens by default.')
166
+ const variantDialogConfirmText = computed(() => variantDialogMode.value === 'create' ? 'Create' : 'Save')
167
+ const deleteVariantSubtitle = computed(() => {
168
+ if (!selectedVariant.value) return 'Delete the selected variant?'
169
+ return `Delete variant “${selectedVariant.value.name}”? This action cannot be undone.`
170
+ })
171
+
172
+ const inlineDateRange = computed(() => {
173
+ const parameters = (definition.value?.parameters ?? []).filter((parameter) => isInlineDateParameterDataType(parameter.dataType))
174
+ if (parameters.length < 2) return null
175
+
176
+ const from = parameters.find((parameter) => {
177
+ const code = normalizeCode(parameter.code)
178
+ return code === 'from_utc' || code === 'frominclusive' || code.endsWith('_from') || code.startsWith('from_')
179
+ })
180
+ const to = parameters.find((parameter) => {
181
+ const code = normalizeCode(parameter.code)
182
+ return code === 'to_utc' || code === 'toinclusive' || code.endsWith('_to') || code.startsWith('to_')
183
+ })
184
+
185
+ return from && to ? {
186
+ fromCode: from.code,
187
+ toCode: to.code,
188
+ fromLabel: parameterLabel(from),
189
+ toLabel: parameterLabel(to),
190
+ title: [from.description, to.description].filter((x) => !!String(x ?? '').trim()).join(' · ') || null,
191
+ } : null
192
+ })
193
+
194
+ const inlineDateParameter = computed(() => {
195
+ if (inlineDateRange.value) return null
196
+ const parameters = (definition.value?.parameters ?? []).filter((parameter) => isInlineDateParameterDataType(parameter.dataType))
197
+ if (parameters.length === 0) return null
198
+
199
+ const preferred = parameters.find((parameter) => {
200
+ const code = normalizeCode(parameter.code)
201
+ return code.includes('as_of') || code === 'period' || code === 'asofperiod'
202
+ })
203
+
204
+ const parameter = (preferred ?? (parameters.length === 1 ? parameters[0] : null)) ?? null
205
+ return parameter ? {
206
+ code: parameter.code,
207
+ label: parameterLabel(parameter),
208
+ hint: parameter.description ?? null,
209
+ } : null
210
+ })
211
+
212
+ const inlineRequiredFilters = computed(() =>
213
+ (definition.value?.filters ?? []).filter((field) => !!field.isRequired && !field.isMulti)
214
+ )
215
+
216
+ const activeParameterBadges = computed<ReportPageBadge[]>(() => {
217
+ if (!definition.value || !draft.value) return []
218
+
219
+ return (definition.value.parameters ?? [])
220
+ .filter((parameter) => {
221
+ const code = parameter.code
222
+ if (inlineDateRange.value && (code === inlineDateRange.value.fromCode || code === inlineDateRange.value.toCode)) return false
223
+ if (inlineDateParameter.value && code === inlineDateParameter.value.code) return false
224
+ return true
225
+ })
226
+ .map((parameter) => {
227
+ const raw = String(draft.value?.parameters[parameter.code] ?? '').trim()
228
+ if (raw.length === 0) return null
229
+ return {
230
+ key: `parameter:${parameter.code}`,
231
+ text: `${parameterLabel(parameter)}: ${raw}`,
232
+ }
233
+ })
234
+ .filter((entry): entry is ReportPageBadge => !!entry)
235
+ })
236
+
237
+ const optionalFilterBadges = computed<ReportPageBadge[]>(() => {
238
+ if (!definition.value || !draft.value) return []
239
+
240
+ return (definition.value.filters ?? [])
241
+ .filter((field) => !field.isRequired)
242
+ .map((field) => {
243
+ const state = draft.value?.filters[field.fieldCode]
244
+ if (!state) return null
245
+
246
+ const itemLabels = state.items
247
+ .map((item) => String(item.label ?? item.id ?? '').trim())
248
+ .filter((label) => label.length > 0)
249
+
250
+ const rawValues = itemLabels.length === 0
251
+ ? splitFilterValues(state.raw).map((value) => tryResolveOptionLabel(field, value))
252
+ : []
253
+ const summary = summarizeFilterValues(itemLabels.length > 0 ? itemLabels : rawValues)
254
+ if (!summary) return null
255
+
256
+ return {
257
+ key: `filter:${field.fieldCode}`,
258
+ text: `${field.label}: ${summary}`,
259
+ }
260
+ })
261
+ .filter((entry): entry is ReportPageBadge => !!entry)
262
+ })
263
+
264
+ const activeBadges = computed(() => [...activeParameterBadges.value, ...optionalFilterBadges.value])
265
+ const currentRouteContext = computed<ReportRouteContext | null>(() => {
266
+ if (!definition.value || !draft.value) return null
267
+
268
+ const context = buildCurrentReportContext(definition.value, draft.value)
269
+ return {
270
+ ...context,
271
+ request: {
272
+ ...context.request,
273
+ limit: resolveDefinitionInitialPageLimit(context.request.limit ?? 500),
274
+ variantCode: activeVariantCode.value || null,
275
+ },
276
+ }
277
+ })
278
+
279
+ const sourceTrail = computed<ReportSourceTrail | null>(() => decodeReportSourceTrailParam(route.query.src))
280
+ const backToSourceUrl = computed(() => buildBackToSourceUrl(sourceTrail.value, resolveBackTarget(route)))
281
+ const currentBackTarget = computed(() => {
282
+ if (!definition.value || !draft.value) return currentRouteBackTarget(route)
283
+ return buildReportPageUrl(reportCode.value, {
284
+ context: currentRouteContext.value,
285
+ sourceTrail: sourceTrail.value,
286
+ backTarget: resolveBackTarget(route),
287
+ })
288
+ })
289
+
290
+ const canLoadMore = computed(() => canAppendReportResponse(response.value) && !loadingDefinition.value && !running.value && !loadingMore.value)
291
+ const reportPresentation = computed(() => definition.value?.presentation ?? null)
292
+ const emptyReportMessage = computed(() => {
293
+ const configured = String(reportPresentation.value?.emptyStateMessage ?? '').trim()
294
+ if (configured.length > 0) return configured
295
+ return 'Open the Composer, adjust filters, rows, measures, or sorting, and run again.'
296
+ })
297
+ const hasPagedExecutionState = computed(() => !!response.value && (response.value.hasMore || consumedAppendCursors.value.length > 0))
298
+ const showEndOfList = computed(() => hasPagedExecutionState.value && !canLoadMore.value && !loadingMore.value && !running.value && (response.value?.sheet.rows?.length ?? 0) > 0)
299
+ const loadedRowCount = computed(() => countLoadedReportRows(response.value?.sheet))
300
+ const totalRowCount = computed(() => {
301
+ const total = response.value?.total
302
+ return typeof total === 'number' && Number.isFinite(total) && total >= 0 ? total : null
303
+ })
304
+ const reportRowNoun = computed(() => {
305
+ const configured = String(reportPresentation.value?.rowNoun ?? '').trim()
306
+ return configured.length > 0 ? configured : 'row'
307
+ })
308
+
309
+ function resolveDefinitionInitialPageLimit(fallback: number) {
310
+ const normalizedFallback = Number.isFinite(fallback) && fallback > 0 ? Math.max(1, Math.floor(fallback)) : 500
311
+ const configured = reportPresentation.value?.initialPageSize
312
+ return typeof configured === 'number' && Number.isFinite(configured) && configured > 0
313
+ ? Math.max(1, Math.floor(configured))
314
+ : normalizedFallback
315
+ }
316
+
317
+ function buildPageExecutionRequest() {
318
+ if (!definition.value || !draft.value) throw new Error('Report definition and draft are required.')
319
+
320
+ const request = buildExecutionRequest(definition.value, draft.value)
321
+ return {
322
+ ...request,
323
+ limit: resolveDefinitionInitialPageLimit(request.limit ?? 500),
324
+ }
325
+ }
326
+
327
+ function clearReportPageSnapshot() {
328
+ clearReportPageExecutionSnapshot(reportPageStateKey.value)
329
+ clearReportPageScrollTop(reportPageStateKey.value)
330
+ }
331
+
332
+ function persistReportExecutionSnapshot() {
333
+ if (!response.value) {
334
+ clearReportPageExecutionSnapshot(reportPageStateKey.value)
335
+ return
336
+ }
337
+
338
+ saveReportPageExecutionSnapshot(reportPageStateKey.value, response.value, consumedAppendCursors.value)
339
+ }
340
+
341
+ function onReportScrollTopChange(scrollTop: number) {
342
+ saveReportPageScrollTop(reportPageStateKey.value, scrollTop)
343
+ }
344
+
345
+ async function restorePendingScrollPosition() {
346
+ const scrollTop = pendingScrollRestore.value
347
+ if (scrollTop <= 0) return
348
+
349
+ pendingScrollRestore.value = 0
350
+ await nextTick()
351
+ reportSheetRef.value?.restoreScrollTop(scrollTop)
352
+ }
353
+
354
+ function tryRestoreReportExecutionSnapshot(): boolean {
355
+ const snapshot = loadReportPageExecutionSnapshot(reportPageStateKey.value)
356
+ if (!snapshot) return false
357
+
358
+ response.value = snapshot.response
359
+ consumedAppendCursors.value = snapshot.consumedCursors
360
+ pendingScrollRestore.value = loadReportPageScrollTop(reportPageStateKey.value)
361
+ error.value = null
362
+ return true
363
+ }
364
+
365
+ function updateDraft(mutator: (next: ReportComposerDraft) => void) {
366
+ if (!draft.value) return
367
+ const next = cloneComposerDraft(draft.value)
368
+ mutator(next)
369
+ draft.value = next
370
+ }
371
+
372
+ function filterState(field: ReportFilterFieldDto) {
373
+ return getReportComposerFilterState(draft.value, field)
374
+ }
375
+
376
+ function selectedFilterItem(field: ReportFilterFieldDto): ReportComposerLookupItem | null {
377
+ return getSelectedReportComposerFilterItem(draft.value, field)
378
+ }
379
+
380
+ function setParameterValue(code: string, value: string | null) {
381
+ updateDraft((next) => {
382
+ next.parameters[code] = value ?? ''
383
+ })
384
+ }
385
+
386
+ function setFilterRaw(fieldCode: string, value: string) {
387
+ updateDraft((next) => {
388
+ const state = next.filters[fieldCode]
389
+ if (!state) return
390
+ state.raw = value
391
+ })
392
+ }
393
+
394
+ function setFilterItem(fieldCode: string, value: unknown) {
395
+ const item = coerceReportComposerLookupItem(value)
396
+ updateDraft((next) => {
397
+ const state = next.filters[fieldCode]
398
+ if (!state) return
399
+ state.items = item ? [{ ...item }] : []
400
+ if (item) state.raw = ''
401
+ })
402
+ }
403
+
404
+ async function openFilterItem(field: ReportFilterFieldDto) {
405
+ const target = await resolveReportLookupTarget({
406
+ hint: field.lookup ?? null,
407
+ value: selectedFilterItem(field),
408
+ routeFullPath: route.fullPath,
409
+ })
410
+
411
+ if (!target) return
412
+ await router.push(target)
413
+ }
414
+
415
+ async function onFilterQuery(payload: { fieldCode: string; query: string }) {
416
+ const field = definition.value?.filters?.find((entry) => entry.fieldCode === payload.fieldCode)
417
+ if (!field?.lookup) return
418
+
419
+ const items = await searchReportLookupItems(lookupStore, field.lookup, payload.query)
420
+ lookupItemsByFilterCode.value = {
421
+ ...lookupItemsByFilterCode.value,
422
+ [payload.fieldCode]: items,
423
+ }
424
+ }
425
+
426
+ async function createDraftFromVariant(definitionValue: ReportDefinitionDto, variant: ReportVariantDto): Promise<ReportComposerDraft> {
427
+ const nextDraft = applyVariantToDraft(definitionValue, variant)
428
+ await hydrateReportLookupItemsFromFilters(lookupStore, definitionValue, nextDraft, variant.filters)
429
+ return nextDraft
430
+ }
431
+
432
+ async function createDraftFromContext(definitionValue: ReportDefinitionDto, context: ReportRouteContext): Promise<ReportComposerDraft> {
433
+ const nextDraft = applyExecutionRequestToDraft(definitionValue, context.request)
434
+ await hydrateReportLookupItemsFromFilters(lookupStore, definitionValue, nextDraft, context.request.filters)
435
+ return nextDraft
436
+ }
437
+
438
+ async function refreshVariants(options?: {
439
+ preferredSelectedVariantCode?: string
440
+ preferredActiveVariantCode?: string
441
+ }) {
442
+ const list = await getReportVariants(reportCode.value)
443
+ variants.value = list
444
+
445
+ const preferredActive = options?.preferredActiveVariantCode ?? activeVariantCode.value
446
+ activeVariantCode.value = preferredActive && list.some((variant) => variant.variantCode === preferredActive)
447
+ ? preferredActive
448
+ : ''
449
+
450
+ const preferredSelected = options?.preferredSelectedVariantCode ?? selectedVariantCode.value
451
+ if (preferredSelected && list.some((variant) => variant.variantCode === preferredSelected)) {
452
+ selectedVariantCode.value = preferredSelected
453
+ return
454
+ }
455
+
456
+ if (activeVariantCode.value && list.some((variant) => variant.variantCode === activeVariantCode.value)) {
457
+ selectedVariantCode.value = activeVariantCode.value
458
+ return
459
+ }
460
+
461
+ selectedVariantCode.value = ''
462
+ }
463
+
464
+ async function syncRouteStateWithCurrentReportContext() {
465
+ if (!definition.value || !draft.value) return
466
+
467
+ const nextQuery: Record<string, unknown> = { ...route.query }
468
+ const nextContext = encodeReportRouteContextParam(currentRouteContext.value)
469
+ const nextSourceTrail = encodeReportSourceTrailParam(sourceTrail.value)
470
+
471
+ if (nextContext) nextQuery.ctx = nextContext
472
+ else delete nextQuery.ctx
473
+
474
+ if (nextSourceTrail) nextQuery.src = nextSourceTrail
475
+ else delete nextQuery.src
476
+
477
+ if (activeVariantCode.value) nextQuery.variant = activeVariantCode.value
478
+ else delete nextQuery.variant
479
+
480
+ const currentCtx = String(route.query.ctx ?? '').trim()
481
+ const currentSrc = String(route.query.src ?? '').trim()
482
+ const currentVariant = String(route.query.variant ?? '').trim()
483
+ const nextVariant = String(nextQuery.variant ?? '').trim()
484
+
485
+ if (currentCtx === String(nextQuery.ctx ?? '').trim()
486
+ && currentSrc === String(nextQuery.src ?? '').trim()
487
+ && currentVariant === nextVariant) return
488
+
489
+ suppressedBootstrapKey.value = stableStringify({
490
+ reportCode: reportCode.value,
491
+ variant: nextQuery.variant ?? null,
492
+ ctx: nextQuery.ctx ?? null,
493
+ src: nextQuery.src ?? null,
494
+ })
495
+
496
+ await setCleanRouteQuery(route, router, nextQuery, 'replace')
497
+ }
498
+
499
+ async function runReport() {
500
+ if (!definition.value || !draft.value) return
501
+
502
+ const seq = ++runSeq
503
+ running.value = true
504
+ loadingMore.value = false
505
+ error.value = null
506
+
507
+ try {
508
+ const result = await executeReport(reportCode.value, buildPageExecutionRequest())
509
+ if (seq !== runSeq) return
510
+
511
+ response.value = result
512
+ consumedAppendCursors.value = []
513
+ pendingScrollRestore.value = 0
514
+ await syncRouteStateWithCurrentReportContext()
515
+ persistReportExecutionSnapshot()
516
+ saveReportPageScrollTop(reportPageStateKey.value, 0)
517
+ } catch (err) {
518
+ if (seq !== runSeq) return
519
+ error.value = toErrorMessage(err, 'Failed to execute the report.')
520
+ } finally {
521
+ if (seq === runSeq) running.value = false
522
+ }
523
+ }
524
+
525
+ async function appendReportPage() {
526
+ if (!definition.value || !draft.value || !response.value) return
527
+
528
+ const nextCursor = String(response.value.nextCursor ?? '').trim()
529
+ if (!nextCursor || loadingMore.value || running.value) return
530
+ if (consumedAppendCursors.value.includes(nextCursor)) return
531
+
532
+ const seq = runSeq
533
+ loadingMore.value = true
534
+ error.value = null
535
+
536
+ try {
537
+ const page = await executeReport(reportCode.value, buildAppendRequest(buildPageExecutionRequest(), nextCursor))
538
+ if (seq !== runSeq) return
539
+
540
+ response.value = mergePagedReportResponses(response.value, page)
541
+ consumedAppendCursors.value = [...consumedAppendCursors.value, nextCursor]
542
+ persistReportExecutionSnapshot()
543
+ } catch (err) {
544
+ if (seq !== runSeq) return
545
+ error.value = toErrorMessage(err, 'Failed to load more rows.')
546
+ } finally {
547
+ if (seq === runSeq) loadingMore.value = false
548
+ }
549
+ }
550
+
551
+ async function downloadReport() {
552
+ if (!definition.value || !draft.value) return
553
+ downloading.value = true
554
+ error.value = null
555
+
556
+ try {
557
+ const file = await exportReportXlsx(reportCode.value, buildExportRequest(definition.value, draft.value))
558
+ const url = URL.createObjectURL(file.blob)
559
+ const link = document.createElement('a')
560
+ link.href = url
561
+ link.download = file.fileName || `${reportCode.value.replace(/[^a-z0-9]+/gi, '-') || 'report'}.xlsx`
562
+ document.body.appendChild(link)
563
+ link.click()
564
+ link.remove()
565
+ URL.revokeObjectURL(url)
566
+ } catch (err) {
567
+ error.value = toErrorMessage(err, 'Failed to export the report.')
568
+ } finally {
569
+ downloading.value = false
570
+ }
571
+ }
572
+
573
+ async function loadSelectedVariant() {
574
+ if (!definition.value) return
575
+
576
+ const variant = selectedVariant.value
577
+ if (!variant) return
578
+
579
+ try {
580
+ draft.value = await createDraftFromVariant(definition.value, variant)
581
+ activeVariantCode.value = variant.variantCode
582
+ selectedVariantCode.value = variant.variantCode
583
+ if (draft.value && canAutoRunReport(definition.value, draft.value)) await runReport()
584
+ else {
585
+ response.value = null
586
+ consumedAppendCursors.value = []
587
+ error.value = null
588
+ await syncRouteStateWithCurrentReportContext()
589
+ clearReportPageSnapshot()
590
+ }
591
+ } catch (err) {
592
+ error.value = toErrorMessage(err, 'Failed to load the report variant.')
593
+ }
594
+ }
595
+
596
+ async function resetToDefault() {
597
+ if (!definition.value) return
598
+
599
+ try {
600
+ const defaultVariant = variants.value.find((variant) => !!variant.isDefault) ?? null
601
+ if (defaultVariant) {
602
+ selectedVariantCode.value = defaultVariant.variantCode
603
+ activeVariantCode.value = defaultVariant.variantCode
604
+ draft.value = await createDraftFromVariant(definition.value, defaultVariant)
605
+ } else {
606
+ selectedVariantCode.value = ''
607
+ activeVariantCode.value = ''
608
+ draft.value = createComposerDraft(definition.value)
609
+ }
610
+
611
+ if (draft.value && canAutoRunReport(definition.value, draft.value)) await runReport()
612
+ else {
613
+ response.value = null
614
+ consumedAppendCursors.value = []
615
+ error.value = null
616
+ await syncRouteStateWithCurrentReportContext()
617
+ clearReportPageSnapshot()
618
+ }
619
+ } catch (err) {
620
+ error.value = toErrorMessage(err, 'Failed to reset the report.')
621
+ }
622
+ }
623
+
624
+ function openCreateVariantDialog() {
625
+ variantDialogMode.value = 'create'
626
+ variantDialogName.value = ''
627
+ variantDialogDefault.value = false
628
+ variantDialogError.value = null
629
+ variantDialogOpen.value = true
630
+ }
631
+
632
+ function openEditVariantDialog() {
633
+ if (!selectedVariant.value) return
634
+
635
+ variantDialogMode.value = 'edit'
636
+ variantDialogName.value = selectedVariant.value.name ?? ''
637
+ variantDialogDefault.value = !!selectedVariant.value.isDefault
638
+ variantDialogError.value = null
639
+ variantDialogOpen.value = true
640
+ }
641
+
642
+ async function submitVariantDialog() {
643
+ if (!definition.value || !draft.value) return
644
+
645
+ const name = String(variantDialogName.value ?? '').trim()
646
+ savingVariant.value = true
647
+ variantDialogError.value = null
648
+ error.value = null
649
+
650
+ try {
651
+ if (variantDialogMode.value === 'create') {
652
+ const variantCode = chooseAvailableVariantCode(name, variants.value)
653
+ const payload = buildVariantDto(definition.value, draft.value, {
654
+ variantCode,
655
+ name,
656
+ isDefault: variantDialogDefault.value,
657
+ isShared: true,
658
+ })
659
+
660
+ const saved = await saveReportVariant(reportCode.value, variantCode, payload)
661
+ activeVariantCode.value = saved.variantCode
662
+ selectedVariantCode.value = saved.variantCode
663
+ await refreshVariants({
664
+ preferredActiveVariantCode: saved.variantCode,
665
+ preferredSelectedVariantCode: saved.variantCode,
666
+ })
667
+ variantDialogOpen.value = false
668
+ await syncRouteStateWithCurrentReportContext()
669
+ return
670
+ }
671
+
672
+ const variant = selectedVariant.value
673
+ if (!variant) return
674
+
675
+ const payload: ReportVariantDto = {
676
+ ...variant,
677
+ name,
678
+ isDefault: variantDialogDefault.value,
679
+ }
680
+
681
+ const saved = await saveReportVariant(reportCode.value, variant.variantCode, payload)
682
+ const shouldKeepActive = activeVariantCode.value === variant.variantCode ? saved.variantCode : activeVariantCode.value
683
+ await refreshVariants({
684
+ preferredActiveVariantCode: shouldKeepActive,
685
+ preferredSelectedVariantCode: saved.variantCode,
686
+ })
687
+ variantDialogOpen.value = false
688
+ await syncRouteStateWithCurrentReportContext()
689
+ } catch (err) {
690
+ variantDialogError.value = toErrorMessage(err, 'Failed to save the report variant.')
691
+ } finally {
692
+ savingVariant.value = false
693
+ }
694
+ }
695
+
696
+ function openDeleteVariantDialog() {
697
+ if (!selectedVariant.value) return
698
+ deleteVariantOpen.value = true
699
+ }
700
+
701
+ async function deleteSelectedVariant() {
702
+ const variant = selectedVariant.value
703
+ if (!variant) return
704
+
705
+ savingVariant.value = true
706
+ error.value = null
707
+
708
+ try {
709
+ await deleteReportVariant(reportCode.value, variant.variantCode)
710
+ deleteVariantOpen.value = false
711
+
712
+ const wasActive = activeVariantCode.value === variant.variantCode
713
+ const preferredSelected = selectedVariantCode.value === variant.variantCode
714
+ ? activeVariantCode.value
715
+ : selectedVariantCode.value
716
+
717
+ if (wasActive) {
718
+ await refreshVariants({
719
+ preferredActiveVariantCode: '',
720
+ preferredSelectedVariantCode: '',
721
+ })
722
+ await resetToDefault()
723
+ return
724
+ }
725
+
726
+ await refreshVariants({
727
+ preferredActiveVariantCode: activeVariantCode.value,
728
+ preferredSelectedVariantCode: preferredSelected,
729
+ })
730
+ await syncRouteStateWithCurrentReportContext()
731
+ } catch (err) {
732
+ error.value = toErrorMessage(err, 'Failed to delete the report variant.')
733
+ } finally {
734
+ savingVariant.value = false
735
+ }
736
+ }
737
+
738
+ async function saveCurrentVariant() {
739
+ if (!definition.value || !draft.value || !activeVariant.value) return
740
+
741
+ savingVariant.value = true
742
+ error.value = null
743
+
744
+ try {
745
+ const saved = await saveReportVariant(reportCode.value, activeVariant.value.variantCode, buildVariantDto(definition.value, draft.value, {
746
+ variantCode: activeVariant.value.variantCode,
747
+ name: activeVariant.value.name,
748
+ isDefault: !!activeVariant.value.isDefault,
749
+ isShared: activeVariant.value.isShared !== false,
750
+ }))
751
+
752
+ activeVariantCode.value = saved.variantCode
753
+ selectedVariantCode.value = saved.variantCode
754
+ await refreshVariants({
755
+ preferredActiveVariantCode: saved.variantCode,
756
+ preferredSelectedVariantCode: saved.variantCode,
757
+ })
758
+ await syncRouteStateWithCurrentReportContext()
759
+ } catch (err) {
760
+ error.value = toErrorMessage(err, 'Failed to save the report variant.')
761
+ } finally {
762
+ savingVariant.value = false
763
+ }
764
+ }
765
+
766
+ async function loadDefinitionAndRun() {
767
+ const code = reportCode.value
768
+ if (!code) return
769
+
770
+ runSeq += 1
771
+ const seq = ++loadSeq
772
+ loadingDefinition.value = true
773
+ running.value = false
774
+ error.value = null
775
+ definition.value = null
776
+ draft.value = null
777
+ response.value = null
778
+ loadingMore.value = false
779
+ composerOpen.value = false
780
+ variantDialogOpen.value = false
781
+ deleteVariantOpen.value = false
782
+ lookupItemsByFilterCode.value = {}
783
+ variants.value = []
784
+ selectedVariantCode.value = ''
785
+ activeVariantCode.value = ''
786
+ consumedAppendCursors.value = []
787
+ pendingScrollRestore.value = 0
788
+
789
+ try {
790
+ const [loadedDefinition, loadedVariants] = await Promise.all([
791
+ getReportDefinition(code),
792
+ getReportVariants(code),
793
+ ])
794
+
795
+ if (seq !== loadSeq) return
796
+
797
+ definition.value = loadedDefinition
798
+ variants.value = loadedVariants
799
+
800
+ const requestedVariantCode = String(route.query.variant ?? '').trim()
801
+ const requestedVariant = requestedVariantCode
802
+ ? loadedVariants.find((variant) => variant.variantCode === requestedVariantCode) ?? null
803
+ : null
804
+ const defaultVariant = loadedVariants.find((variant) => !!variant.isDefault) ?? null
805
+ const routeContext = decodeReportRouteContextParam(route.query.ctx)
806
+
807
+ if (routeContext && routeContext.reportCode === loadedDefinition.reportCode) {
808
+ const contextVariantCode = String(routeContext.request.variantCode ?? requestedVariantCode).trim()
809
+ activeVariantCode.value = contextVariantCode && loadedVariants.some((variant) => variant.variantCode === contextVariantCode)
810
+ ? contextVariantCode
811
+ : ''
812
+ selectedVariantCode.value = activeVariantCode.value
813
+ draft.value = await createDraftFromContext(loadedDefinition, routeContext)
814
+ } else if (requestedVariant) {
815
+ selectedVariantCode.value = requestedVariant.variantCode
816
+ activeVariantCode.value = requestedVariant.variantCode
817
+ draft.value = await createDraftFromVariant(loadedDefinition, requestedVariant)
818
+ } else if (defaultVariant) {
819
+ selectedVariantCode.value = defaultVariant.variantCode
820
+ activeVariantCode.value = defaultVariant.variantCode
821
+ draft.value = await createDraftFromVariant(loadedDefinition, defaultVariant)
822
+ } else {
823
+ selectedVariantCode.value = ''
824
+ activeVariantCode.value = ''
825
+ draft.value = createComposerDraft(loadedDefinition)
826
+ }
827
+
828
+ if (tryRestoreReportExecutionSnapshot()) {
829
+ await restorePendingScrollPosition()
830
+ return
831
+ }
832
+
833
+ if (draft.value && canAutoRunReport(loadedDefinition, draft.value)) await runReport()
834
+ else {
835
+ response.value = null
836
+ error.value = null
837
+ clearReportPageSnapshot()
838
+ }
839
+ } catch (err) {
840
+ if (seq !== loadSeq) return
841
+ error.value = toErrorMessage(err, 'Failed to load the report definition.')
842
+ } finally {
843
+ if (seq === loadSeq) loadingDefinition.value = false
844
+ }
845
+ }
846
+
847
+ const commandPaletteActions = computed<CommandPaletteItemSeed[]>(() => {
848
+ const actions: CommandPaletteItemSeed[] = []
849
+
850
+ if (activeVariant.value) {
851
+ actions.push({
852
+ key: 'current:save-variant',
853
+ group: 'actions',
854
+ kind: 'command',
855
+ scope: 'commands',
856
+ title: 'Save current variant',
857
+ subtitle: activeVariant.value.name,
858
+ icon: 'save',
859
+ badge: 'Save',
860
+ hint: null,
861
+ route: null,
862
+ commandCode: 'save-variant',
863
+ status: null,
864
+ openInNewTabSupported: false,
865
+ keywords: ['save variant', activeVariant.value.name],
866
+ defaultRank: 978,
867
+ isCurrentContext: true,
868
+ perform: saveCurrentVariant,
869
+ })
870
+ }
871
+
872
+ if (selectedVariant.value && selectedVariantCode.value !== activeVariantCode.value) {
873
+ actions.push({
874
+ key: 'current:load-variant',
875
+ group: 'actions',
876
+ kind: 'command',
877
+ scope: 'commands',
878
+ title: 'Load selected variant',
879
+ subtitle: selectedVariant.value.name,
880
+ icon: 'load-variant',
881
+ badge: 'Load',
882
+ hint: null,
883
+ route: null,
884
+ commandCode: 'load-variant',
885
+ status: null,
886
+ openInNewTabSupported: false,
887
+ keywords: ['load variant', selectedVariant.value.name],
888
+ defaultRank: 976,
889
+ isCurrentContext: true,
890
+ perform: loadSelectedVariant,
891
+ })
892
+ }
893
+
894
+ return actions
895
+ })
896
+
897
+ useCommandPalettePageContext(() => ({
898
+ entityType: 'report',
899
+ documentType: null,
900
+ catalogType: null,
901
+ entityId: null,
902
+ title: pageTitle.value,
903
+ actions: commandPaletteActions.value,
904
+ }))
905
+
906
+ watch(reportPageStateKey, (nextKey, prevKey) => {
907
+ if (nextKey === prevKey || !response.value) return
908
+ persistReportExecutionSnapshot()
909
+ }, { flush: 'post' })
910
+
911
+ watch(response, async (nextResponse) => {
912
+ if (!nextResponse) return
913
+ await restorePendingScrollPosition()
914
+ }, { flush: 'post' })
915
+
916
+ watch(routeBootstrapKey, (nextKey) => {
917
+ if (suppressedBootstrapKey.value === nextKey) {
918
+ suppressedBootstrapKey.value = null
919
+ return
920
+ }
921
+
922
+ void loadDefinitionAndRun()
923
+ }, { immediate: true })
924
+ </script>
925
+
926
+ <template>
927
+ <div class="h-full min-h-0 flex flex-col" data-testid="report-page">
928
+ <NgbPageHeader :title="pageTitle" can-back @back="navigateBack(router, route, backToSourceUrl)">
929
+ <template #secondary>
930
+ <div v-if="pageSubtitle" class="text-xs text-ngb-muted truncate">{{ pageSubtitle }}</div>
931
+ </template>
932
+ <template #actions>
933
+ <div class="flex flex-wrap items-center justify-end gap-2">
934
+ <div
935
+ v-for="field in inlineRequiredFilters"
936
+ :key="field.fieldCode"
937
+ class="w-[17rem] max-w-full"
938
+ >
939
+ <NgbLookup
940
+ v-if="field.lookup"
941
+ :model-value="selectedFilterItem(field)"
942
+ :items="lookupItemsByFilterCode[field.fieldCode] ?? []"
943
+ :disabled="loadingDefinition || running || downloading || !draft"
944
+ :show-open="!!selectedFilterItem(field)"
945
+ :show-clear="!!selectedFilterItem(field)"
946
+ :placeholder="`Type ${field.label.toLowerCase()}…`"
947
+ variant="compact"
948
+ @query="onFilterQuery({ fieldCode: field.fieldCode, query: $event })"
949
+ @update:model-value="setFilterItem(field.fieldCode, $event)"
950
+ @open="void openFilterItem(field)"
951
+ />
952
+
953
+ <NgbInput
954
+ v-else
955
+ :model-value="filterState(field).raw"
956
+ :disabled="loadingDefinition || running || downloading || !draft"
957
+ :placeholder="field.label"
958
+ @update:model-value="setFilterRaw(field.fieldCode, String($event ?? ''))"
959
+ />
960
+ </div>
961
+
962
+ <DocumentDateRangeFilter
963
+ v-if="inlineDateRange && draft"
964
+ :from-date="String(draft.parameters[inlineDateRange.fromCode] ?? '')"
965
+ :to-date="String(draft.parameters[inlineDateRange.toCode] ?? '')"
966
+ :from-placeholder="inlineDateRange.fromLabel"
967
+ :to-placeholder="inlineDateRange.toLabel"
968
+ :title="inlineDateRange.title ?? undefined"
969
+ :disabled="loadingDefinition || running || downloading || !draft"
970
+ @update:from-date="setParameterValue(inlineDateRange.fromCode, $event)"
971
+ @update:to-date="setParameterValue(inlineDateRange.toCode, $event)"
972
+ />
973
+
974
+ <div
975
+ v-else-if="inlineDateParameter && draft"
976
+ class="relative inline-flex h-[26px] items-stretch rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card text-xs shadow-card"
977
+ :title="inlineDateParameter.hint ?? undefined"
978
+ >
979
+ <div class="h-full w-[10rem]">
980
+ <NgbDatePicker
981
+ :model-value="normalizeReportDateValue(draft.parameters[inlineDateParameter.code])"
982
+ :placeholder="inlineDateParameter.label"
983
+ grouped
984
+ :disabled="loadingDefinition || running || downloading || !draft"
985
+ @update:model-value="setParameterValue(inlineDateParameter.code, $event)"
986
+ />
987
+ </div>
988
+ </div>
989
+
990
+ <div class="mx-1 h-5 w-px bg-ngb-border" aria-hidden="true" />
991
+
992
+ <div class="flex items-center gap-1.5">
993
+ <button type="button" class="ngb-iconbtn" :disabled="loadingDefinition || running || !definition || !draft" title="Run" @click="runReport">
994
+ <NgbIcon name="play" :size="50" />
995
+ </button>
996
+ <button type="button" class="ngb-iconbtn" :disabled="loadingDefinition || !definition" title="Composer" @click="composerOpen = true">
997
+ <NgbIcon name="composer" />
998
+ </button>
999
+ <button type="button" class="ngb-iconbtn" :disabled="loadingDefinition || running || downloading || !definition || !draft || !definition?.capabilities?.allowsXlsxExport" title="Download" @click="downloadReport">
1000
+ <NgbIcon name="download" />
1001
+ </button>
1002
+ </div>
1003
+ </div>
1004
+ </template>
1005
+ </NgbPageHeader>
1006
+
1007
+ <div class="flex-1 min-h-0 flex flex-col gap-4 overflow-hidden p-6" data-testid="report-page-content">
1008
+ <div v-if="error" class="rounded-[var(--ngb-radius)] border border-red-200 bg-red-50 p-3 text-sm text-red-900 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-100">
1009
+ {{ error }}
1010
+ </div>
1011
+
1012
+ <div v-if="activeBadges.length > 0" class="-mb-1 flex flex-wrap items-center gap-2" data-testid="report-page-active-badges">
1013
+ <NgbBadge v-for="badge in activeBadges" :key="badge.key" tone="neutral">{{ badge.text }}</NgbBadge>
1014
+ </div>
1015
+
1016
+ <div
1017
+ v-if="loadingDefinition && !definition"
1018
+ class="rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-5 shadow-card"
1019
+ >
1020
+ <div class="text-sm font-semibold text-ngb-text">Loading report definition…</div>
1021
+ <div class="mt-2 text-sm text-ngb-muted">Fetching the report metadata and default layout.</div>
1022
+ </div>
1023
+
1024
+ <div v-else-if="definition" class="flex min-h-0 flex-1 flex-col overflow-hidden">
1025
+ <ReportSheet
1026
+ ref="reportSheetRef"
1027
+ class="min-h-0 flex-1"
1028
+ :sheet="response?.sheet ?? null"
1029
+ :loading="running"
1030
+ :loading-more="loadingMore"
1031
+ :can-load-more="canLoadMore"
1032
+ :current-report-context="currentRouteContext"
1033
+ :source-trail="sourceTrail"
1034
+ :back-target="currentBackTarget"
1035
+ :show-end-of-list="showEndOfList"
1036
+ :loaded-count="loadedRowCount"
1037
+ :total-count="totalRowCount"
1038
+ :row-noun="reportRowNoun"
1039
+ empty-title="No rows for this layout"
1040
+ :empty-message="emptyReportMessage"
1041
+ @load-more="appendReportPage"
1042
+ @scroll-top-change="onReportScrollTopChange"
1043
+ />
1044
+ </div>
1045
+ </div>
1046
+ </div>
1047
+
1048
+ <NgbDrawer v-model:open="composerOpen" title="" hide-header flush-body>
1049
+ <ReportComposerPanel
1050
+ v-if="definition && draft"
1051
+ :definition="definition"
1052
+ :model-value="draft"
1053
+ :lookup-items-by-filter-code="lookupItemsByFilterCode"
1054
+ :running="running"
1055
+ :variant-options="variantOptions"
1056
+ :selected-variant-code="selectedVariantCode"
1057
+ :variant-summary="variantSummary"
1058
+ :variant-disabled="loadingDefinition || savingVariant || !definition || !draft"
1059
+ :create-variant-disabled="createVariantDisabled"
1060
+ :edit-variant-disabled="editVariantDisabled"
1061
+ :save-variant-disabled="saveVariantDisabled"
1062
+ :delete-variant-disabled="deleteVariantDisabled"
1063
+ :reset-variant-disabled="resetVariantDisabled"
1064
+ :load-variant-disabled="loadVariantDisabled"
1065
+ @update:model-value="draft = $event"
1066
+ @filter-query="onFilterQuery"
1067
+ @update:selected-variant-code="selectedVariantCode = $event"
1068
+ @create-variant="openCreateVariantDialog"
1069
+ @edit-variant="openEditVariantDialog"
1070
+ @save-variant="saveCurrentVariant"
1071
+ @delete-variant="openDeleteVariantDialog"
1072
+ @reset-variant="resetToDefault"
1073
+ @load-variant="loadSelectedVariant"
1074
+ @run="runReport(); composerOpen = false"
1075
+ @close="composerOpen = false"
1076
+ />
1077
+ </NgbDrawer>
1078
+
1079
+ <NgbDialog
1080
+ :open="variantDialogOpen"
1081
+ :title="variantDialogTitle"
1082
+ :subtitle="variantDialogSubtitle"
1083
+ :confirm-text="variantDialogConfirmText"
1084
+ cancel-text="Cancel"
1085
+ :confirm-loading="savingVariant"
1086
+ @update:open="variantDialogOpen = $event; variantDialogError = null"
1087
+ @confirm="submitVariantDialog"
1088
+ >
1089
+ <div class="space-y-4">
1090
+ <div v-if="variantDialogError" class="rounded-[var(--ngb-radius)] border border-red-200 bg-red-50 p-3 text-sm text-red-900 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-100">
1091
+ {{ variantDialogError }}
1092
+ </div>
1093
+
1094
+ <div>
1095
+ <div class="mb-1 text-sm font-medium text-ngb-text">Variant name</div>
1096
+ <NgbInput v-model="variantDialogName" placeholder="Month-end ledger" />
1097
+ </div>
1098
+
1099
+ <div class="flex items-center justify-between gap-3 rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card px-3 py-2">
1100
+ <div>
1101
+ <div class="text-sm font-medium text-ngb-text">Set as default</div>
1102
+ <div class="text-xs text-ngb-muted">Use this variant automatically when the page opens.</div>
1103
+ </div>
1104
+ <NgbSwitch v-model="variantDialogDefault" />
1105
+ </div>
1106
+ </div>
1107
+ </NgbDialog>
1108
+
1109
+ <NgbDialog
1110
+ :open="deleteVariantOpen"
1111
+ title="Delete variant"
1112
+ :subtitle="deleteVariantSubtitle"
1113
+ confirm-text="Delete"
1114
+ cancel-text="Cancel"
1115
+ danger
1116
+ :confirm-loading="savingVariant"
1117
+ @update:open="deleteVariantOpen = $event"
1118
+ @confirm="deleteSelectedVariant"
1119
+ >
1120
+ <div class="text-sm text-ngb-muted">
1121
+ The current report draft will stay unchanged unless you delete the variant that is currently loaded.
1122
+ </div>
1123
+ </NgbDialog>
1124
+ </template>