@ngbplatform/ui 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +44 -0
- package/package.json +61 -0
- package/public/favicon.svg +6 -0
- package/public/silent-check-sso.html +8 -0
- package/src/assets/ngb_logo.svg +17 -0
- package/src/assets/ngb_logo_color.svg +17 -0
- package/src/index.ts +1023 -0
- package/src/ngb/README.md +27 -0
- package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
- package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
- package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
- package/src/ngb/accounting/api.ts +79 -0
- package/src/ngb/accounting/generalJournalEntry.ts +127 -0
- package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
- package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
- package/src/ngb/accounting/navigation.ts +83 -0
- package/src/ngb/accounting/periodClosing.ts +75 -0
- package/src/ngb/accounting/periodClosingApi.ts +60 -0
- package/src/ngb/accounting/periodClosingTypes.ts +73 -0
- package/src/ngb/accounting/types.ts +57 -0
- package/src/ngb/api/audit.ts +23 -0
- package/src/ngb/api/catalogs.ts +50 -0
- package/src/ngb/api/contracts.ts +285 -0
- package/src/ngb/api/documents.ts +170 -0
- package/src/ngb/api/http.ts +446 -0
- package/src/ngb/api/lookups.ts +14 -0
- package/src/ngb/api/types.ts +6 -0
- package/src/ngb/auth/index.ts +13 -0
- package/src/ngb/auth/keycloak.ts +374 -0
- package/src/ngb/auth/router.ts +35 -0
- package/src/ngb/auth/types.ts +12 -0
- package/src/ngb/auth/useAuthStore.ts +209 -0
- package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
- package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
- package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
- package/src/ngb/command-palette/api.ts +10 -0
- package/src/ngb/command-palette/config.ts +16 -0
- package/src/ngb/command-palette/presentation.ts +85 -0
- package/src/ngb/command-palette/search.ts +130 -0
- package/src/ngb/command-palette/staticItems.ts +243 -0
- package/src/ngb/command-palette/storage.ts +45 -0
- package/src/ngb/command-palette/store.ts +716 -0
- package/src/ngb/command-palette/types.ts +120 -0
- package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
- package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
- package/src/ngb/components/NgbConfirmDialog.vue +63 -0
- package/src/ngb/components/NgbDialog.vue +44 -0
- package/src/ngb/components/NgbDrawer.vue +156 -0
- package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
- package/src/ngb/components/NgbModalShell.vue +99 -0
- package/src/ngb/components/NgbToolbar.vue +14 -0
- package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
- package/src/ngb/components/forms/NgbFormRow.vue +21 -0
- package/src/ngb/components/forms/NgbFormSection.vue +16 -0
- package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
- package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
- package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
- package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
- package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
- package/src/ngb/components/register/registerTypes.ts +51 -0
- package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
- package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
- package/src/ngb/components/register/useRegisterRows.ts +209 -0
- package/src/ngb/components/register/useRegisterViewport.ts +56 -0
- package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
- package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
- package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
- package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
- package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
- package/src/ngb/editor/NgbEntityEditor.vue +250 -0
- package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
- package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
- package/src/ngb/editor/catalogNavigation.ts +13 -0
- package/src/ngb/editor/config.ts +252 -0
- package/src/ngb/editor/documentCopyDraft.ts +245 -0
- package/src/ngb/editor/documentEffects.ts +43 -0
- package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
- package/src/ngb/editor/documentNavigation.ts +115 -0
- package/src/ngb/editor/documentStatus.ts +45 -0
- package/src/ngb/editor/entityEditorErrors.ts +162 -0
- package/src/ngb/editor/entityEditorPersistence.ts +241 -0
- package/src/ngb/editor/extensions.ts +40 -0
- package/src/ngb/editor/types.ts +295 -0
- package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
- package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
- package/src/ngb/editor/useEditorDrawerState.ts +88 -0
- package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
- package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
- package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
- package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
- package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
- package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
- package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
- package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
- package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
- package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
- package/src/ngb/env/runtimeConfig.ts +25 -0
- package/src/ngb/lookup/config.ts +42 -0
- package/src/ngb/lookup/defaultConfig.ts +136 -0
- package/src/ngb/lookup/navigation.ts +76 -0
- package/src/ngb/lookup/prefetch.ts +40 -0
- package/src/ngb/lookup/store.ts +261 -0
- package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
- package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
- package/src/ngb/metadata/NgbEntityForm.vue +84 -0
- package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
- package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
- package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
- package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
- package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
- package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
- package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
- package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
- package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
- package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
- package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
- package/src/ngb/metadata/config.ts +28 -0
- package/src/ngb/metadata/dataTypes.ts +27 -0
- package/src/ngb/metadata/entityForm.ts +114 -0
- package/src/ngb/metadata/entityModel.ts +34 -0
- package/src/ngb/metadata/entityValueFormatting.ts +83 -0
- package/src/ngb/metadata/fieldRendererState.ts +84 -0
- package/src/ngb/metadata/filtering.ts +153 -0
- package/src/ngb/metadata/lookup.ts +25 -0
- package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
- package/src/ngb/metadata/normalization.ts +100 -0
- package/src/ngb/metadata/referenceHydration.ts +99 -0
- package/src/ngb/metadata/register.ts +135 -0
- package/src/ngb/metadata/routePages.ts +86 -0
- package/src/ngb/metadata/store.ts +35 -0
- package/src/ngb/metadata/types.ts +276 -0
- package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
- package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
- package/src/ngb/metadata/useValidationFocus.ts +49 -0
- package/src/ngb/primitives/NgbBadge.vue +31 -0
- package/src/ngb/primitives/NgbButton.vue +53 -0
- package/src/ngb/primitives/NgbCheckbox.vue +33 -0
- package/src/ngb/primitives/NgbDatePicker.vue +141 -0
- package/src/ngb/primitives/NgbIcon.vue +545 -0
- package/src/ngb/primitives/NgbInput.vue +57 -0
- package/src/ngb/primitives/NgbLookup.vue +264 -0
- package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
- package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
- package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
- package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
- package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
- package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
- package/src/ngb/primitives/NgbSelect.vue +108 -0
- package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
- package/src/ngb/primitives/NgbSwitch.vue +42 -0
- package/src/ngb/primitives/NgbTabs.vue +116 -0
- package/src/ngb/primitives/NgbToastHost.vue +49 -0
- package/src/ngb/primitives/NgbTooltip.vue +27 -0
- package/src/ngb/primitives/iconNames.ts +83 -0
- package/src/ngb/primitives/toast.ts +66 -0
- package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
- package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
- package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
- package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
- package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
- package/src/ngb/reporting/NgbReportPage.vue +1124 -0
- package/src/ngb/reporting/NgbReportSheet.vue +467 -0
- package/src/ngb/reporting/api.ts +35 -0
- package/src/ngb/reporting/composer.ts +904 -0
- package/src/ngb/reporting/config.ts +120 -0
- package/src/ngb/reporting/defaultConfig.ts +15 -0
- package/src/ngb/reporting/lookupFilters.ts +43 -0
- package/src/ngb/reporting/navigation.ts +181 -0
- package/src/ngb/reporting/pageHelpers.ts +73 -0
- package/src/ngb/reporting/pageSession.ts +92 -0
- package/src/ngb/reporting/paging.ts +66 -0
- package/src/ngb/reporting/types.ts +333 -0
- package/src/ngb/router/backNavigation.ts +122 -0
- package/src/ngb/router/queryParams.ts +129 -0
- package/src/ngb/router/queryState.ts +121 -0
- package/src/ngb/router/routeAliases.ts +66 -0
- package/src/ngb/router/routeParams.ts +15 -0
- package/src/ngb/router/shareLink.ts +39 -0
- package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
- package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
- package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
- package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
- package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
- package/src/ngb/security/NgbRolesPage.vue +125 -0
- package/src/ngb/security/NgbUserEditorPage.vue +615 -0
- package/src/ngb/security/NgbUsersPage.vue +125 -0
- package/src/ngb/security/api.ts +91 -0
- package/src/ngb/security/audit.ts +21 -0
- package/src/ngb/security/permissions.ts +65 -0
- package/src/ngb/security/types.ts +157 -0
- package/src/ngb/security/useAccessStore.ts +69 -0
- package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
- package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
- package/src/ngb/site/NgbLogo.vue +44 -0
- package/src/ngb/site/NgbPageHeader.vue +49 -0
- package/src/ngb/site/NgbSiteShell.vue +286 -0
- package/src/ngb/site/NgbSiteSidebar.vue +270 -0
- package/src/ngb/site/NgbTopBar.vue +425 -0
- package/src/ngb/site/NgbTrendChart.vue +224 -0
- package/src/ngb/site/dashboardData.ts +324 -0
- package/src/ngb/site/mainMenuStore.ts +49 -0
- package/src/ngb/site/types.ts +31 -0
- package/src/ngb/site/useDashboardPageState.ts +98 -0
- package/src/ngb/site/useTheme.ts +95 -0
- package/src/ngb/utils/clone.ts +43 -0
- package/src/ngb/utils/dateValues.ts +107 -0
- package/src/ngb/utils/errorMessage.ts +14 -0
- package/src/ngb/utils/guid.ts +21 -0
- package/src/ngb/utils/stableValue.ts +21 -0
- package/src/ngb/utils/storage.ts +135 -0
- package/src/styles/tailwind.css +165 -0
- package/src/vite/publicAssets.ts +104 -0
- package/vite-public-assets.js +68 -0
|
@@ -0,0 +1,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>
|