@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,1187 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
4
|
+
import NgbButton from '../primitives/NgbButton.vue'
|
|
5
|
+
import NgbDatePicker from '../primitives/NgbDatePicker.vue'
|
|
6
|
+
import NgbIcon from '../primitives/NgbIcon.vue'
|
|
7
|
+
import NgbInput from '../primitives/NgbInput.vue'
|
|
8
|
+
import NgbSelect from '../primitives/NgbSelect.vue'
|
|
9
|
+
import NgbSwitch from '../primitives/NgbSwitch.vue'
|
|
10
|
+
import NgbTabs from '../primitives/NgbTabs.vue'
|
|
11
|
+
import NgbFormLayout from '../components/forms/NgbFormLayout.vue'
|
|
12
|
+
import NgbFormRow from '../components/forms/NgbFormRow.vue'
|
|
13
|
+
import FilterFieldControl from '../metadata/NgbFilterFieldControl.vue'
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
ReportSortDirection,
|
|
17
|
+
ReportTimeGrain,
|
|
18
|
+
type ReportComposerDraft,
|
|
19
|
+
type ReportComposerLookupItem,
|
|
20
|
+
type ReportDefinitionDto,
|
|
21
|
+
type ReportFieldDto,
|
|
22
|
+
type ReportFilterFieldDto,
|
|
23
|
+
type ReportOptionItem,
|
|
24
|
+
} from './types'
|
|
25
|
+
import {
|
|
26
|
+
coerceReportAggregationKind,
|
|
27
|
+
coerceReportComposerLookupItems,
|
|
28
|
+
coerceReportSortDirection,
|
|
29
|
+
coerceReportTimeGrain,
|
|
30
|
+
cloneComposerDraft,
|
|
31
|
+
getAggregationOptions,
|
|
32
|
+
getReportComposerFilterState,
|
|
33
|
+
getSelectedReportComposerFilterItem,
|
|
34
|
+
getGroupableFields,
|
|
35
|
+
getMeasureOptions,
|
|
36
|
+
getSelectableFields,
|
|
37
|
+
getSortableFields,
|
|
38
|
+
getTimeGrainOptions,
|
|
39
|
+
normalizeComposerDraft,
|
|
40
|
+
resolveDefaultAggregation,
|
|
41
|
+
resolveMeasureLabel,
|
|
42
|
+
sortDirectionOptions,
|
|
43
|
+
} from './composer'
|
|
44
|
+
import { resolveReportLookupTarget } from './config'
|
|
45
|
+
import ReportComposerCollectionSection from './NgbReportComposerCollectionSection.vue'
|
|
46
|
+
|
|
47
|
+
const props = defineProps<{
|
|
48
|
+
definition: ReportDefinitionDto
|
|
49
|
+
modelValue: ReportComposerDraft
|
|
50
|
+
lookupItemsByFilterCode: Record<string, ReportComposerLookupItem[]>
|
|
51
|
+
running?: boolean
|
|
52
|
+
variantOptions?: ReportOptionItem[]
|
|
53
|
+
selectedVariantCode?: string
|
|
54
|
+
variantSummary?: string
|
|
55
|
+
variantDisabled?: boolean
|
|
56
|
+
createVariantDisabled?: boolean
|
|
57
|
+
editVariantDisabled?: boolean
|
|
58
|
+
saveVariantDisabled?: boolean
|
|
59
|
+
deleteVariantDisabled?: boolean
|
|
60
|
+
resetVariantDisabled?: boolean
|
|
61
|
+
loadVariantDisabled?: boolean
|
|
62
|
+
}>()
|
|
63
|
+
|
|
64
|
+
const emit = defineEmits<{
|
|
65
|
+
(e: 'update:modelValue', value: ReportComposerDraft): void
|
|
66
|
+
(e: 'run'): void
|
|
67
|
+
(e: 'close'): void
|
|
68
|
+
(e: 'filter-query', payload: { fieldCode: string; query: string }): void
|
|
69
|
+
(e: 'update:selectedVariantCode', value: string): void
|
|
70
|
+
(e: 'create-variant'): void
|
|
71
|
+
(e: 'edit-variant'): void
|
|
72
|
+
(e: 'save-variant'): void
|
|
73
|
+
(e: 'delete-variant'): void
|
|
74
|
+
(e: 'reset-variant'): void
|
|
75
|
+
(e: 'load-variant'): void
|
|
76
|
+
}>()
|
|
77
|
+
|
|
78
|
+
type ComposerTabKey = 'general' | 'grouping' | 'filters' | 'fields' | 'sorting' | 'variant'
|
|
79
|
+
type ComposerDragSection = 'rowGroups' | 'columnGroups' | 'measures' | 'detailFields' | 'sorts'
|
|
80
|
+
type GroupingEntry = ReportComposerDraft['rowGroups'][number]
|
|
81
|
+
type MeasureEntry = ReportComposerDraft['measures'][number]
|
|
82
|
+
type SortEntry = ReportComposerDraft['sorts'][number]
|
|
83
|
+
|
|
84
|
+
const groupableFields = computed(() => getGroupableFields(props.definition))
|
|
85
|
+
const selectableFields = computed(() => getSelectableFields(props.definition))
|
|
86
|
+
const sortableFields = computed(() => getSortableFields(props.definition))
|
|
87
|
+
const measures = computed(() => getMeasureOptions(props.definition))
|
|
88
|
+
const route = useRoute()
|
|
89
|
+
const router = useRouter()
|
|
90
|
+
const sortDirections = sortDirectionOptions()
|
|
91
|
+
const groupingSectionColumns = [
|
|
92
|
+
{ title: 'Field' },
|
|
93
|
+
{ title: 'Time grain', width: '220px' },
|
|
94
|
+
]
|
|
95
|
+
const measureSectionColumns = [
|
|
96
|
+
{ title: 'Measure' },
|
|
97
|
+
{ title: 'Aggregation', width: '132px' },
|
|
98
|
+
{ title: 'Label', width: '132px' },
|
|
99
|
+
]
|
|
100
|
+
const detailFieldSectionColumns = [
|
|
101
|
+
{ title: 'Field' },
|
|
102
|
+
]
|
|
103
|
+
const sortingSectionColumns = [
|
|
104
|
+
{ title: 'Field', width: '120px' },
|
|
105
|
+
{ title: 'Axis', width: '120px' },
|
|
106
|
+
{ title: 'Direction', width: '132px' },
|
|
107
|
+
{ title: 'Time grain', width: '100px' },
|
|
108
|
+
]
|
|
109
|
+
const allowsRowGroups = computed(() => !!props.definition.capabilities?.allowsRowGroups)
|
|
110
|
+
const allowsColumnGroups = computed(() => !!props.definition.capabilities?.allowsColumnGroups)
|
|
111
|
+
const allowsMeasures = computed(() => props.definition.capabilities?.allowsMeasures !== false)
|
|
112
|
+
const allowsDetailFields = computed(() => !!props.definition.capabilities?.allowsDetailFields)
|
|
113
|
+
const allowsSorting = computed(() => !!props.definition.capabilities?.allowsSorting)
|
|
114
|
+
const allowsShowDetails = computed(() => !!props.definition.capabilities?.allowsShowDetails)
|
|
115
|
+
const allowsSubtotals = computed(() => !!props.definition.capabilities?.allowsSubtotals)
|
|
116
|
+
const allowsSeparateRowSubtotals = computed(() => !!props.definition.capabilities?.allowsSeparateRowSubtotals)
|
|
117
|
+
const allowsGrandTotals = computed(() => props.definition.capabilities?.allowsGrandTotals !== false)
|
|
118
|
+
const allowsVariants = computed(() => props.definition.capabilities?.allowsVariants !== false)
|
|
119
|
+
|
|
120
|
+
type SortTarget = {
|
|
121
|
+
fieldCode: string
|
|
122
|
+
timeGrain: ReportTimeGrain | null
|
|
123
|
+
appliesToColumnAxis: boolean
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const timeGrainDrillOrder = [
|
|
127
|
+
ReportTimeGrain.Year,
|
|
128
|
+
ReportTimeGrain.Quarter,
|
|
129
|
+
ReportTimeGrain.Month,
|
|
130
|
+
ReportTimeGrain.Week,
|
|
131
|
+
ReportTimeGrain.Day,
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
const groupFieldOptions = computed(() => groupableFields.value.map((field) => ({ value: field.code, label: field.label })))
|
|
135
|
+
const selectedSortableFields = computed(() => {
|
|
136
|
+
const selected = new Map<string, ReportFieldDto>()
|
|
137
|
+
|
|
138
|
+
for (const group of props.modelValue.rowGroups) {
|
|
139
|
+
const field = fieldByCode(group.fieldCode)
|
|
140
|
+
if (field?.isSortable) selected.set(field.code, field)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const group of props.modelValue.columnGroups) {
|
|
144
|
+
const field = fieldByCode(group.fieldCode)
|
|
145
|
+
if (field?.isSortable) selected.set(field.code, field)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const fieldCode of props.modelValue.detailFields) {
|
|
149
|
+
const field = fieldByCode(fieldCode)
|
|
150
|
+
if (field?.isSortable) selected.set(field.code, field)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return [...selected.values()]
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const sortFieldOptions = computed(() => selectedSortableFields.value.map((field) => ({ value: field.code, label: field.label })))
|
|
157
|
+
const availableSortTargets = computed<SortTarget[]>(() => {
|
|
158
|
+
const targets: SortTarget[] = []
|
|
159
|
+
const seen = new Set<string>()
|
|
160
|
+
|
|
161
|
+
for (const group of props.modelValue.rowGroups) {
|
|
162
|
+
const field = fieldByCode(group.fieldCode)
|
|
163
|
+
if (!field?.isSortable) continue
|
|
164
|
+
|
|
165
|
+
const key = buildAxisSortTargetKey(group.fieldCode, group.timeGrain ?? null, false)
|
|
166
|
+
if (seen.has(key)) continue
|
|
167
|
+
seen.add(key)
|
|
168
|
+
targets.push({
|
|
169
|
+
fieldCode: group.fieldCode,
|
|
170
|
+
timeGrain: group.timeGrain ?? null,
|
|
171
|
+
appliesToColumnAxis: false,
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const group of props.modelValue.columnGroups) {
|
|
176
|
+
const field = fieldByCode(group.fieldCode)
|
|
177
|
+
if (!field?.isSortable) continue
|
|
178
|
+
|
|
179
|
+
const key = buildAxisSortTargetKey(group.fieldCode, group.timeGrain ?? null, true)
|
|
180
|
+
if (seen.has(key)) continue
|
|
181
|
+
seen.add(key)
|
|
182
|
+
targets.push({
|
|
183
|
+
fieldCode: group.fieldCode,
|
|
184
|
+
timeGrain: group.timeGrain ?? null,
|
|
185
|
+
appliesToColumnAxis: true,
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const fieldCode of props.modelValue.detailFields) {
|
|
190
|
+
const field = fieldByCode(fieldCode)
|
|
191
|
+
if (!field?.isSortable) continue
|
|
192
|
+
|
|
193
|
+
const key = buildAxisSortTargetKey(fieldCode, null, false)
|
|
194
|
+
if (seen.has(key)) continue
|
|
195
|
+
seen.add(key)
|
|
196
|
+
targets.push({
|
|
197
|
+
fieldCode,
|
|
198
|
+
timeGrain: null,
|
|
199
|
+
appliesToColumnAxis: false,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return targets
|
|
204
|
+
})
|
|
205
|
+
const measureOptions = computed(() => measures.value.map((measure) => ({ value: measure.code, label: measure.label })))
|
|
206
|
+
const selectableFieldOptions = computed(() => selectableFields.value.map((field) => ({ value: field.code, label: field.label, meta: field.description ?? undefined })))
|
|
207
|
+
const sortingHelpLines = computed(() => {
|
|
208
|
+
if (!allowsSorting.value || sortableFields.value.length === 0) return []
|
|
209
|
+
|
|
210
|
+
const lines = ['Sort only by fields already selected in row groups, column groups, or detail fields.']
|
|
211
|
+
const hasTimeGrouping = [...props.modelValue.rowGroups, ...props.modelValue.columnGroups]
|
|
212
|
+
.some((group) => {
|
|
213
|
+
const field = fieldByCode(group.fieldCode)
|
|
214
|
+
return (field?.supportedTimeGrains?.length ?? 0) > 0
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
if (hasTimeGrouping) {
|
|
218
|
+
lines.push('For grouped time fields, the time grain must match the selected grouping axis.')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return lines
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const hasGeneralTab = computed(() => (props.definition.parameters?.length ?? 0) > 0 || hasFormattingContent.value)
|
|
225
|
+
const hasGroupingTab = computed(() => (allowsRowGroups.value || allowsColumnGroups.value) && groupableFields.value.length > 0)
|
|
226
|
+
const hasFiltersTab = computed(() => (props.definition.filters?.length ?? 0) > 0)
|
|
227
|
+
const hasFieldsTab = computed(() => (allowsMeasures.value && measureOptions.value.length > 0) || (allowsDetailFields.value && selectableFieldOptions.value.length > 0))
|
|
228
|
+
const hasSortingTab = computed(() => allowsSorting.value && sortableFields.value.length > 0)
|
|
229
|
+
const hasFormattingContent = computed(() => allowsShowDetails.value || allowsSubtotals.value || allowsGrandTotals.value)
|
|
230
|
+
const hasVariantTab = computed(() => allowsVariants.value)
|
|
231
|
+
|
|
232
|
+
const tabs = computed(() => {
|
|
233
|
+
const items: { key: ComposerTabKey; label: string }[] = []
|
|
234
|
+
if (hasGeneralTab.value) items.push({ key: 'general', label: 'General' })
|
|
235
|
+
if (hasGroupingTab.value) items.push({ key: 'grouping', label: 'Grouping' })
|
|
236
|
+
if (hasFiltersTab.value) items.push({ key: 'filters', label: 'Filters' })
|
|
237
|
+
if (hasFieldsTab.value) items.push({ key: 'fields', label: 'Fields' })
|
|
238
|
+
if (hasSortingTab.value) items.push({ key: 'sorting', label: 'Sorting' })
|
|
239
|
+
if (hasVariantTab.value) items.push({ key: 'variant', label: 'Variant' })
|
|
240
|
+
return items
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const activeTab = ref<ComposerTabKey>('general')
|
|
244
|
+
const dragState = ref<{ section: ComposerDragSection; index: number } | null>(null)
|
|
245
|
+
|
|
246
|
+
watch(
|
|
247
|
+
tabs,
|
|
248
|
+
(nextTabs) => {
|
|
249
|
+
const first = nextTabs[0]?.key ?? 'general'
|
|
250
|
+
if (!nextTabs.some((tab) => tab.key === activeTab.value)) activeTab.value = first
|
|
251
|
+
},
|
|
252
|
+
{ immediate: true },
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
function updateDraft(mutator: (draft: ReportComposerDraft) => void) {
|
|
256
|
+
const next = cloneComposerDraft(props.modelValue)
|
|
257
|
+
mutator(next)
|
|
258
|
+
emit('update:modelValue', normalizeComposerDraft(props.definition, next))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function rowGroupingKey(group: GroupingEntry, index: number): string {
|
|
262
|
+
return `row:${group.fieldCode}:${index}`
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function columnGroupingKey(group: GroupingEntry, index: number): string {
|
|
266
|
+
return `column:${group.fieldCode}:${index}`
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function measureKey(measure: MeasureEntry, index: number): string {
|
|
270
|
+
return `${measure.measureCode}:${index}`
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function detailFieldKey(fieldCode: string, index: number): string {
|
|
274
|
+
return `${fieldCode}:${index}`
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function sortKey(sort: SortEntry, index: number): string {
|
|
278
|
+
return `${sort.fieldCode}:${sort.timeGrain ?? 'none'}:${index}`
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const rowGroupingRowKey = (item: unknown, index: number) => rowGroupingKey(item as GroupingEntry, index)
|
|
282
|
+
const columnGroupingRowKey = (item: unknown, index: number) => columnGroupingKey(item as GroupingEntry, index)
|
|
283
|
+
const measureRowKey = (item: unknown, index: number) => measureKey(item as MeasureEntry, index)
|
|
284
|
+
const detailFieldRowKey = (item: unknown, index: number) => detailFieldKey(String(item ?? ''), index)
|
|
285
|
+
const sortRowKey = (item: unknown, index: number) => sortKey(item as SortEntry, index)
|
|
286
|
+
|
|
287
|
+
function groupingTimeGrainOptions(fieldCode: string): ReportOptionItem[] {
|
|
288
|
+
return [{ value: '', label: 'No time grain' }, ...getTimeGrainOptions(fieldByCode(fieldCode))]
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function hasGroupingTimeGrainOptions(fieldCode: string): boolean {
|
|
292
|
+
return getTimeGrainOptions(fieldByCode(fieldCode)).length > 0
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function measureAggregationOptions(measureCode: string): ReportOptionItem[] {
|
|
296
|
+
return getAggregationOptions(props.definition, measureCode)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function measureAggregationValue(measure: MeasureEntry) {
|
|
300
|
+
return measure.aggregation ?? resolveDefaultAggregation(props.definition, measure.measureCode)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function handleCollectionDragStart(payload: { section: string; index: number; event: DragEvent }) {
|
|
304
|
+
onComposerDragStart(payload.section as ComposerDragSection, payload.index, payload.event)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function handleCollectionDrop(payload: { section: string; index: number; event: DragEvent }) {
|
|
308
|
+
onComposerDrop(payload.section as ComposerDragSection, payload.index, payload.event)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function filterState(field: ReportFilterFieldDto) {
|
|
312
|
+
return getReportComposerFilterState(props.modelValue, field)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function selectedFilterItem(field: ReportFilterFieldDto): ReportComposerLookupItem | null {
|
|
316
|
+
return getSelectedReportComposerFilterItem(props.modelValue, field)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function normalizeParameterDataType(dataType: string): string {
|
|
320
|
+
return dataType.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_')
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function isDateParameter(dataType: string) {
|
|
324
|
+
const normalized = normalizeParameterDataType(dataType)
|
|
325
|
+
return normalized === 'date' || normalized === 'date_only' || normalized === 'date_time_utc'
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function parameterInputType(dataType: string): string {
|
|
329
|
+
const normalized = dataType.trim().toLowerCase()
|
|
330
|
+
if (normalized.includes('int') || normalized.includes('decimal') || normalized.includes('number')) return 'number'
|
|
331
|
+
return 'text'
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function normalizeDateParameterValue(value: string | null | undefined): string | null {
|
|
335
|
+
const text = String(value ?? '').trim()
|
|
336
|
+
const match = text.match(/^(\d{4}-\d{2}-\d{2})/)
|
|
337
|
+
return match?.[1] ?? null
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function fieldByCode(fieldCode: string): ReportFieldDto | undefined {
|
|
341
|
+
return props.definition.dataset?.fields?.find((field) => field.code === fieldCode)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function buildSortTargetKey(fieldCode: string, timeGrain: ReportTimeGrain | null): string {
|
|
345
|
+
return `${fieldCode}|${timeGrain ?? ''}`
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function buildAxisSortTargetKey(fieldCode: string, timeGrain: ReportTimeGrain | null, appliesToColumnAxis: boolean): string {
|
|
349
|
+
return `${fieldCode}|${appliesToColumnAxis ? 'column' : 'row'}|${timeGrain ?? ''}`
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function nextFinerTimeGrain(field: ReportFieldDto | undefined, current: ReportTimeGrain | null): ReportTimeGrain | null {
|
|
353
|
+
if (!field || current === null) return null
|
|
354
|
+
|
|
355
|
+
const supported = new Set(getTimeGrainOptions(field).map((option) => option.value))
|
|
356
|
+
const currentIndex = timeGrainDrillOrder.indexOf(current)
|
|
357
|
+
if (currentIndex < 0) return null
|
|
358
|
+
|
|
359
|
+
for (let index = currentIndex + 1; index < timeGrainDrillOrder.length; index += 1) {
|
|
360
|
+
const grain = timeGrainDrillOrder[index]!
|
|
361
|
+
if (supported.has(grain)) return grain
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return null
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function nextDrillGrouping(axis: 'rowGroups' | 'columnGroups'): { fieldCode: string; timeGrain: ReportTimeGrain | null } | null {
|
|
368
|
+
const groups = props.modelValue[axis]
|
|
369
|
+
const last = groups[groups.length - 1]
|
|
370
|
+
if (!last) return null
|
|
371
|
+
|
|
372
|
+
const field = fieldByCode(last.fieldCode)
|
|
373
|
+
const nextGrain = nextFinerTimeGrain(field, last.timeGrain)
|
|
374
|
+
if (nextGrain === null) return null
|
|
375
|
+
|
|
376
|
+
const existing = new Set(groups.map((entry) => buildSortTargetKey(entry.fieldCode, entry.timeGrain ?? null)))
|
|
377
|
+
const key = buildSortTargetKey(last.fieldCode, nextGrain)
|
|
378
|
+
if (existing.has(key)) return null
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
fieldCode: last.fieldCode,
|
|
382
|
+
timeGrain: nextGrain,
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function sortAxisOptionsForField(fieldCode: string): ReportOptionItem[] {
|
|
387
|
+
const options: ReportOptionItem[] = []
|
|
388
|
+
|
|
389
|
+
if (props.modelValue.rowGroups.some((group) => group.fieldCode === fieldCode))
|
|
390
|
+
options.push({ value: 'row', label: 'Rows' })
|
|
391
|
+
|
|
392
|
+
if (props.modelValue.columnGroups.some((group) => group.fieldCode === fieldCode))
|
|
393
|
+
options.push({ value: 'column', label: 'Columns' })
|
|
394
|
+
|
|
395
|
+
if (props.modelValue.detailFields.includes(fieldCode))
|
|
396
|
+
options.push({ value: 'detail', label: 'Details' })
|
|
397
|
+
|
|
398
|
+
return options
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function resolveSortAxis(sort: { fieldCode: string; appliesToColumnAxis?: boolean }): 'row' | 'column' | 'detail' {
|
|
402
|
+
const available = sortAxisOptionsForField(sort.fieldCode).map((option) => String(option.value))
|
|
403
|
+
|
|
404
|
+
if (sort.appliesToColumnAxis && available.includes('column')) return 'column'
|
|
405
|
+
if (!sort.appliesToColumnAxis && available.includes('row')) return 'row'
|
|
406
|
+
if (!sort.appliesToColumnAxis && available.includes('detail')) return 'detail'
|
|
407
|
+
if (available.includes('column')) return 'column'
|
|
408
|
+
if (available.includes('detail')) return 'detail'
|
|
409
|
+
return 'row'
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function sortTimeGrainOptionsForFieldAndAxis(fieldCode: string, axis: 'row' | 'column' | 'detail'): ReportOptionItem[] {
|
|
413
|
+
const field = fieldByCode(fieldCode)
|
|
414
|
+
if (!field || axis === 'detail') return []
|
|
415
|
+
|
|
416
|
+
const groups = axis === 'column' ? props.modelValue.columnGroups : props.modelValue.rowGroups
|
|
417
|
+
|
|
418
|
+
const groupedTimeGrains = Array.from(new Set(
|
|
419
|
+
groups
|
|
420
|
+
.filter((group): group is typeof group & { timeGrain: ReportTimeGrain } => group.fieldCode === fieldCode && group.timeGrain !== null)
|
|
421
|
+
.map((group) => group.timeGrain),
|
|
422
|
+
))
|
|
423
|
+
|
|
424
|
+
if (groupedTimeGrains.length === 0) return []
|
|
425
|
+
|
|
426
|
+
return groupedTimeGrains.map((grain) => ({
|
|
427
|
+
value: grain,
|
|
428
|
+
label: getTimeGrainOptions(field).find((option) => option.value === grain)?.label ?? 'No time grain',
|
|
429
|
+
}))
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function addGrouping(axis: 'rowGroups' | 'columnGroups') {
|
|
433
|
+
const fallback = groupableFields.value[0]
|
|
434
|
+
if (!fallback) return
|
|
435
|
+
|
|
436
|
+
const drill = nextDrillGrouping(axis)
|
|
437
|
+
if (drill) {
|
|
438
|
+
updateDraft((draft) => {
|
|
439
|
+
draft[axis].push({
|
|
440
|
+
fieldCode: drill.fieldCode,
|
|
441
|
+
timeGrain: drill.timeGrain,
|
|
442
|
+
})
|
|
443
|
+
})
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const existing = new Set(props.modelValue[axis].map((entry) => entry.fieldCode))
|
|
448
|
+
const chosen = groupableFields.value.find((field) => !existing.has(field.code)) ?? fallback
|
|
449
|
+
|
|
450
|
+
updateDraft((draft) => {
|
|
451
|
+
draft[axis].push({
|
|
452
|
+
fieldCode: chosen.code,
|
|
453
|
+
timeGrain: null,
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function moveGrouping(axis: 'rowGroups' | 'columnGroups', index: number, delta: number) {
|
|
459
|
+
updateDraft((draft) => {
|
|
460
|
+
const target = index + delta
|
|
461
|
+
if (target < 0 || target >= draft[axis].length) return
|
|
462
|
+
const [entry] = draft[axis].splice(index, 1)
|
|
463
|
+
draft[axis].splice(target, 0, entry!)
|
|
464
|
+
})
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function removeGrouping(axis: 'rowGroups' | 'columnGroups', index: number) {
|
|
468
|
+
updateDraft((draft) => {
|
|
469
|
+
draft[axis].splice(index, 1)
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function setGroupingField(axis: 'rowGroups' | 'columnGroups', index: number, fieldCode: string) {
|
|
474
|
+
updateDraft((draft) => {
|
|
475
|
+
const entry = draft[axis][index]
|
|
476
|
+
if (!entry) return
|
|
477
|
+
entry.fieldCode = fieldCode
|
|
478
|
+
const field = fieldByCode(fieldCode)
|
|
479
|
+
if (!field?.supportedTimeGrains?.length) entry.timeGrain = null
|
|
480
|
+
})
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function setGroupingTimeGrain(axis: 'rowGroups' | 'columnGroups', index: number, value: unknown) {
|
|
484
|
+
updateDraft((draft) => {
|
|
485
|
+
const entry = draft[axis][index]
|
|
486
|
+
if (!entry) return
|
|
487
|
+
entry.timeGrain = coerceReportTimeGrain(value)
|
|
488
|
+
})
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function addMeasure() {
|
|
492
|
+
const fallback = measures.value[0]
|
|
493
|
+
if (!fallback) return
|
|
494
|
+
|
|
495
|
+
const existing = new Set(props.modelValue.measures.map((entry) => entry.measureCode))
|
|
496
|
+
const chosen = measures.value.find((measure) => !existing.has(measure.code)) ?? fallback
|
|
497
|
+
|
|
498
|
+
updateDraft((draft) => {
|
|
499
|
+
draft.measures.push({
|
|
500
|
+
measureCode: chosen.code,
|
|
501
|
+
aggregation: resolveDefaultAggregation(props.definition, chosen.code),
|
|
502
|
+
labelOverride: null,
|
|
503
|
+
})
|
|
504
|
+
})
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function addSort() {
|
|
508
|
+
const fallback = availableSortTargets.value[0]
|
|
509
|
+
if (!fallback) return
|
|
510
|
+
|
|
511
|
+
const existing = new Set(props.modelValue.sorts.map((entry) => buildAxisSortTargetKey(entry.fieldCode, entry.timeGrain ?? null, !!entry.appliesToColumnAxis)))
|
|
512
|
+
const chosen = availableSortTargets.value.find((target) => !existing.has(buildAxisSortTargetKey(target.fieldCode, target.timeGrain, target.appliesToColumnAxis))) ?? fallback
|
|
513
|
+
|
|
514
|
+
updateDraft((draft) => {
|
|
515
|
+
draft.sorts.push({
|
|
516
|
+
fieldCode: chosen.fieldCode,
|
|
517
|
+
appliesToColumnAxis: chosen.appliesToColumnAxis,
|
|
518
|
+
groupKey: null,
|
|
519
|
+
direction: ReportSortDirection.Asc,
|
|
520
|
+
timeGrain: chosen.timeGrain,
|
|
521
|
+
})
|
|
522
|
+
})
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function moveSort(index: number, delta: number) {
|
|
526
|
+
updateDraft((draft) => {
|
|
527
|
+
const target = index + delta
|
|
528
|
+
if (target < 0 || target >= draft.sorts.length) return
|
|
529
|
+
const [entry] = draft.sorts.splice(index, 1)
|
|
530
|
+
draft.sorts.splice(target, 0, entry!)
|
|
531
|
+
})
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function removeMeasure(index: number) {
|
|
535
|
+
updateDraft((draft) => {
|
|
536
|
+
draft.measures.splice(index, 1)
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function removeSort(index: number) {
|
|
541
|
+
updateDraft((draft) => {
|
|
542
|
+
draft.sorts.splice(index, 1)
|
|
543
|
+
})
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function setParameter(code: string, value: string) {
|
|
547
|
+
updateDraft((draft) => {
|
|
548
|
+
draft.parameters[code] = value
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function setFilterRaw(fieldCode: string, value: string) {
|
|
553
|
+
updateDraft((draft) => {
|
|
554
|
+
const state = draft.filters[fieldCode]
|
|
555
|
+
if (!state) return
|
|
556
|
+
state.raw = value
|
|
557
|
+
})
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function setFilterItems(fieldCode: string, value: unknown) {
|
|
561
|
+
const items = coerceReportComposerLookupItems(value)
|
|
562
|
+
updateDraft((draft) => {
|
|
563
|
+
const state = draft.filters[fieldCode]
|
|
564
|
+
if (!state) return
|
|
565
|
+
state.items = items.map((item) => ({ ...item }))
|
|
566
|
+
if (items.length > 0) state.raw = ''
|
|
567
|
+
})
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function openFilterItem(field: ReportFilterFieldDto) {
|
|
571
|
+
const target = await resolveReportLookupTarget({
|
|
572
|
+
hint: field.lookup ?? null,
|
|
573
|
+
value: selectedFilterItem(field),
|
|
574
|
+
routeFullPath: route.fullPath,
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
if (!target) return
|
|
578
|
+
await router.push(target)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function setFilterIncludeDescendants(fieldCode: string, value: boolean) {
|
|
582
|
+
updateDraft((draft) => {
|
|
583
|
+
const state = draft.filters[fieldCode]
|
|
584
|
+
if (!state) return
|
|
585
|
+
state.includeDescendants = value
|
|
586
|
+
})
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function setMeasureCode(index: number, measureCode: string) {
|
|
590
|
+
updateDraft((draft) => {
|
|
591
|
+
const entry = draft.measures[index]
|
|
592
|
+
if (!entry) return
|
|
593
|
+
entry.measureCode = measureCode
|
|
594
|
+
entry.aggregation = resolveDefaultAggregation(props.definition, measureCode)
|
|
595
|
+
entry.labelOverride = null
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function setMeasureAggregation(index: number, value: unknown) {
|
|
600
|
+
updateDraft((draft) => {
|
|
601
|
+
const entry = draft.measures[index]
|
|
602
|
+
if (!entry) return
|
|
603
|
+
entry.aggregation = coerceReportAggregationKind(value)
|
|
604
|
+
})
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function measureLabelValue(index: number): string {
|
|
608
|
+
const entry = props.modelValue.measures[index]
|
|
609
|
+
return entry ? resolveMeasureLabel(props.definition, entry) : ''
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function setMeasureLabel(index: number, value: string) {
|
|
613
|
+
updateDraft((draft) => {
|
|
614
|
+
const entry = draft.measures[index]
|
|
615
|
+
if (!entry) return
|
|
616
|
+
|
|
617
|
+
const trimmed = String(value ?? '').trim()
|
|
618
|
+
const autoLabel = resolveMeasureLabel(props.definition, { ...entry, labelOverride: null })
|
|
619
|
+
entry.labelOverride = trimmed.length === 0 || trimmed === autoLabel ? null : trimmed
|
|
620
|
+
})
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function addDetailField() {
|
|
624
|
+
const fallback = selectableFields.value[0]
|
|
625
|
+
if (!fallback) return
|
|
626
|
+
|
|
627
|
+
const existing = new Set(props.modelValue.detailFields)
|
|
628
|
+
const chosen = selectableFields.value.find((field) => !existing.has(field.code)) ?? fallback
|
|
629
|
+
|
|
630
|
+
updateDraft((draft) => {
|
|
631
|
+
draft.detailFields.push(chosen.code)
|
|
632
|
+
})
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function removeDetailField(index: number) {
|
|
636
|
+
updateDraft((draft) => {
|
|
637
|
+
draft.detailFields.splice(index, 1)
|
|
638
|
+
})
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function detailFieldOptionsFor(currentFieldCode: string) {
|
|
642
|
+
const selected = new Set(props.modelValue.detailFields.filter((fieldCode) => fieldCode !== currentFieldCode))
|
|
643
|
+
return selectableFieldOptions.value.filter((option) => !selected.has(String(option.value)))
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function sortFieldOptionsFor(_currentFieldCode: string) {
|
|
647
|
+
return sortFieldOptions.value
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function isSortTimeGrainLocked(fieldCode: string, axis: 'row' | 'column' | 'detail'): boolean {
|
|
651
|
+
return sortTimeGrainOptionsForFieldAndAxis(fieldCode, axis).length <= 1
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function setDetailField(index: number, fieldCode: string) {
|
|
655
|
+
updateDraft((draft) => {
|
|
656
|
+
if (fieldCode.trim().length === 0) return
|
|
657
|
+
const duplicateIndex = draft.detailFields.findIndex((entry, entryIndex) => entry === fieldCode && entryIndex !== index)
|
|
658
|
+
if (duplicateIndex >= 0) return
|
|
659
|
+
draft.detailFields[index] = fieldCode
|
|
660
|
+
})
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function setSortField(index: number, fieldCode: string) {
|
|
664
|
+
updateDraft((draft) => {
|
|
665
|
+
const entry = draft.sorts[index]
|
|
666
|
+
if (!entry) return
|
|
667
|
+
entry.fieldCode = fieldCode
|
|
668
|
+
entry.groupKey = null
|
|
669
|
+
const firstAxis = String(sortAxisOptionsForField(fieldCode)[0]?.value ?? 'row')
|
|
670
|
+
const axis = firstAxis === 'column' ? 'column' : firstAxis === 'detail' ? 'detail' : 'row'
|
|
671
|
+
entry.appliesToColumnAxis = axis === 'column'
|
|
672
|
+
const options = sortTimeGrainOptionsForFieldAndAxis(fieldCode, axis)
|
|
673
|
+
entry.timeGrain = axis === 'detail' ? null : coerceReportTimeGrain(options[0]?.value ?? null)
|
|
674
|
+
})
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function setSortAxis(index: number, value: string) {
|
|
678
|
+
updateDraft((draft) => {
|
|
679
|
+
const entry = draft.sorts[index]
|
|
680
|
+
if (!entry) return
|
|
681
|
+
|
|
682
|
+
const axis = value === 'column' ? 'column' : value === 'detail' ? 'detail' : 'row'
|
|
683
|
+
entry.appliesToColumnAxis = axis === 'column'
|
|
684
|
+
entry.groupKey = null
|
|
685
|
+
const options = sortTimeGrainOptionsForFieldAndAxis(entry.fieldCode, axis)
|
|
686
|
+
entry.timeGrain = axis === 'detail' ? null : coerceReportTimeGrain(options[0]?.value ?? null)
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function setSortDirection(index: number, value: unknown) {
|
|
691
|
+
updateDraft((draft) => {
|
|
692
|
+
const entry = draft.sorts[index]
|
|
693
|
+
if (!entry) return
|
|
694
|
+
entry.direction = coerceReportSortDirection(value) ?? ReportSortDirection.Asc
|
|
695
|
+
})
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function setSortTimeGrain(index: number, value: unknown) {
|
|
699
|
+
updateDraft((draft) => {
|
|
700
|
+
const entry = draft.sorts[index]
|
|
701
|
+
if (!entry) return
|
|
702
|
+
entry.timeGrain = coerceReportTimeGrain(value)
|
|
703
|
+
})
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function moveItem<T>(items: T[], from: number, to: number) {
|
|
707
|
+
if (from === to || from < 0 || to < 0 || from >= items.length || to >= items.length) return
|
|
708
|
+
const [entry] = items.splice(from, 1)
|
|
709
|
+
items.splice(to, 0, entry!)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function onComposerDragStart(section: ComposerDragSection, index: number, e: DragEvent) {
|
|
713
|
+
dragState.value = { section, index }
|
|
714
|
+
try {
|
|
715
|
+
e.dataTransfer?.setData('text/plain', String(index))
|
|
716
|
+
e.dataTransfer?.setDragImage(new Image(), 0, 0)
|
|
717
|
+
} catch {
|
|
718
|
+
// ignore
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function onComposerDragOver(e: DragEvent) {
|
|
723
|
+
e.preventDefault()
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function onComposerDrop(section: ComposerDragSection, index: number, e: DragEvent) {
|
|
727
|
+
e.preventDefault()
|
|
728
|
+
|
|
729
|
+
const active = dragState.value
|
|
730
|
+
const from = active?.section === section
|
|
731
|
+
? active.index
|
|
732
|
+
: Number(e.dataTransfer?.getData('text/plain') ?? NaN)
|
|
733
|
+
|
|
734
|
+
if (!Number.isFinite(from) || from === index) {
|
|
735
|
+
dragState.value = null
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
updateDraft((draft) => {
|
|
740
|
+
switch (section) {
|
|
741
|
+
case 'rowGroups':
|
|
742
|
+
moveItem(draft.rowGroups, from, index)
|
|
743
|
+
break
|
|
744
|
+
case 'columnGroups':
|
|
745
|
+
moveItem(draft.columnGroups, from, index)
|
|
746
|
+
break
|
|
747
|
+
case 'measures':
|
|
748
|
+
moveItem(draft.measures, from, index)
|
|
749
|
+
break
|
|
750
|
+
case 'detailFields':
|
|
751
|
+
moveItem(draft.detailFields, from, index)
|
|
752
|
+
break
|
|
753
|
+
case 'sorts':
|
|
754
|
+
moveItem(draft.sorts, from, index)
|
|
755
|
+
break
|
|
756
|
+
}
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
dragState.value = null
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function setFlag(flag: 'showDetails' | 'showSubtotals' | 'showSubtotalsOnSeparateRows' | 'showGrandTotals', value: boolean) {
|
|
763
|
+
updateDraft((draft) => {
|
|
764
|
+
draft[flag] = value
|
|
765
|
+
})
|
|
766
|
+
}
|
|
767
|
+
</script>
|
|
768
|
+
|
|
769
|
+
<template>
|
|
770
|
+
<div data-testid="report-composer-panel" class="flex h-full flex-col">
|
|
771
|
+
<div class="flex items-center gap-3 border-b border-ngb-border px-5 py-4">
|
|
772
|
+
<div class="min-w-0 flex-1">
|
|
773
|
+
<div class="truncate text-base font-semibold text-ngb-text">{{ definition.name }}</div>
|
|
774
|
+
<div v-if="definition.description" class="mt-1 text-sm text-ngb-muted">{{ definition.description }}</div>
|
|
775
|
+
</div>
|
|
776
|
+
|
|
777
|
+
<div class="flex items-center gap-2">
|
|
778
|
+
<button type="button" class="ngb-iconbtn" :disabled="running" title="Run" @click="emit('run')">
|
|
779
|
+
<NgbIcon name="play" :size="50" />
|
|
780
|
+
</button>
|
|
781
|
+
<button type="button" class="ngb-iconbtn" title="Close" @click="emit('close')">
|
|
782
|
+
<NgbIcon name="x" />
|
|
783
|
+
</button>
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
786
|
+
|
|
787
|
+
<div class="flex-1 overflow-auto px-5 py-4">
|
|
788
|
+
<NgbTabs v-model="activeTab" :tabs="tabs" full-width-bar>
|
|
789
|
+
<template #default="{ active }">
|
|
790
|
+
<div class="space-y-6">
|
|
791
|
+
<section v-if="active === 'general' && hasGeneralTab" class="space-y-6">
|
|
792
|
+
<NgbFormLayout v-if="(definition.parameters?.length ?? 0) > 0">
|
|
793
|
+
<NgbFormRow
|
|
794
|
+
v-for="parameter in definition.parameters ?? []"
|
|
795
|
+
:key="parameter.code"
|
|
796
|
+
:label="parameter.label ?? parameter.code"
|
|
797
|
+
:hint="parameter.description ?? undefined"
|
|
798
|
+
dense
|
|
799
|
+
>
|
|
800
|
+
<NgbDatePicker
|
|
801
|
+
v-if="isDateParameter(parameter.dataType)"
|
|
802
|
+
:model-value="normalizeDateParameterValue(modelValue.parameters[parameter.code])"
|
|
803
|
+
@update:model-value="setParameter(parameter.code, $event ?? '')"
|
|
804
|
+
/>
|
|
805
|
+
|
|
806
|
+
<NgbInput
|
|
807
|
+
v-else
|
|
808
|
+
:model-value="modelValue.parameters[parameter.code] ?? ''"
|
|
809
|
+
:type="parameterInputType(parameter.dataType)"
|
|
810
|
+
:placeholder="parameter.dataType"
|
|
811
|
+
@update:model-value="setParameter(parameter.code, String($event ?? ''))"
|
|
812
|
+
/>
|
|
813
|
+
</NgbFormRow>
|
|
814
|
+
</NgbFormLayout>
|
|
815
|
+
|
|
816
|
+
<div v-if="hasFormattingContent" class="grid gap-3">
|
|
817
|
+
<div v-if="allowsShowDetails" class="flex items-center justify-between gap-4 rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-4 shadow-card">
|
|
818
|
+
<div>
|
|
819
|
+
<div class="text-sm font-medium text-ngb-text">Show details</div>
|
|
820
|
+
<div class="mt-1 text-xs text-ngb-muted">Ask the engine to include detail rows under groups when supported.</div>
|
|
821
|
+
</div>
|
|
822
|
+
<NgbSwitch :model-value="modelValue.showDetails" @update:model-value="setFlag('showDetails', $event)" />
|
|
823
|
+
</div>
|
|
824
|
+
|
|
825
|
+
<div v-if="allowsSubtotals" class="flex items-center justify-between gap-4 rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-4 shadow-card">
|
|
826
|
+
<div>
|
|
827
|
+
<div class="text-sm font-medium text-ngb-text">Show subtotals</div>
|
|
828
|
+
<div class="mt-1 text-xs text-ngb-muted">Render subtotal values for grouped output and row-axis pivot slices.</div>
|
|
829
|
+
</div>
|
|
830
|
+
<NgbSwitch :model-value="modelValue.showSubtotals" @update:model-value="setFlag('showSubtotals', $event)" />
|
|
831
|
+
</div>
|
|
832
|
+
|
|
833
|
+
<div v-if="allowsSeparateRowSubtotals" class="flex items-center justify-between gap-4 rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-4 shadow-card">
|
|
834
|
+
<div>
|
|
835
|
+
<div class="text-sm font-medium text-ngb-text">Show subtotals on separate rows</div>
|
|
836
|
+
<div class="mt-1 text-xs text-ngb-muted">Keep the hierarchy column, move subtotals to dedicated rows.</div>
|
|
837
|
+
</div>
|
|
838
|
+
<NgbSwitch
|
|
839
|
+
:model-value="modelValue.showSubtotalsOnSeparateRows"
|
|
840
|
+
:disabled="!modelValue.showSubtotals"
|
|
841
|
+
@update:model-value="setFlag('showSubtotalsOnSeparateRows', $event)"
|
|
842
|
+
/>
|
|
843
|
+
</div>
|
|
844
|
+
|
|
845
|
+
<div v-if="allowsGrandTotals" class="flex items-center justify-between gap-4 rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-4 shadow-card">
|
|
846
|
+
<div>
|
|
847
|
+
<div class="text-sm font-medium text-ngb-text">Show totals</div>
|
|
848
|
+
<div class="mt-1 text-xs text-ngb-muted">Render trailing total columns and the final total row for pivot layouts.</div>
|
|
849
|
+
</div>
|
|
850
|
+
<NgbSwitch :model-value="modelValue.showGrandTotals" @update:model-value="setFlag('showGrandTotals', $event)" />
|
|
851
|
+
</div>
|
|
852
|
+
</div>
|
|
853
|
+
|
|
854
|
+
<div v-if="sortingHelpLines.length > 0" class="space-y-1 text-xs text-ngb-muted">
|
|
855
|
+
<p v-for="line in sortingHelpLines" :key="line">{{ line }}</p>
|
|
856
|
+
</div>
|
|
857
|
+
</section>
|
|
858
|
+
|
|
859
|
+
<section v-if="active === 'grouping' && hasGroupingTab" class="space-y-6">
|
|
860
|
+
<ReportComposerCollectionSection
|
|
861
|
+
v-if="allowsRowGroups"
|
|
862
|
+
title="Rows"
|
|
863
|
+
add-label="Add row"
|
|
864
|
+
:items="modelValue.rowGroups"
|
|
865
|
+
:columns="groupingSectionColumns"
|
|
866
|
+
empty-message="No row groups selected."
|
|
867
|
+
section="rowGroups"
|
|
868
|
+
:row-key="rowGroupingRowKey"
|
|
869
|
+
@add="addGrouping('rowGroups')"
|
|
870
|
+
@remove="removeGrouping('rowGroups', $event)"
|
|
871
|
+
@dragstart="handleCollectionDragStart"
|
|
872
|
+
@dragover="onComposerDragOver"
|
|
873
|
+
@drop="handleCollectionDrop"
|
|
874
|
+
>
|
|
875
|
+
<template #cells="{ item: group, index }">
|
|
876
|
+
<td class="border-r border-dotted border-ngb-border px-0 py-1 align-top">
|
|
877
|
+
<div class="px-1">
|
|
878
|
+
<NgbSelect
|
|
879
|
+
variant="grid"
|
|
880
|
+
:model-value="group.fieldCode"
|
|
881
|
+
:options="groupFieldOptions"
|
|
882
|
+
@update:model-value="setGroupingField('rowGroups', index, String($event ?? ''))"
|
|
883
|
+
/>
|
|
884
|
+
</div>
|
|
885
|
+
</td>
|
|
886
|
+
<td class="border-r border-dotted border-ngb-border px-0 py-1 align-top">
|
|
887
|
+
<div class="px-1">
|
|
888
|
+
<NgbSelect
|
|
889
|
+
variant="grid"
|
|
890
|
+
:model-value="group.timeGrain ?? ''"
|
|
891
|
+
:options="groupingTimeGrainOptions(group.fieldCode)"
|
|
892
|
+
:disabled="!hasGroupingTimeGrainOptions(group.fieldCode)"
|
|
893
|
+
@update:model-value="setGroupingTimeGrain('rowGroups', index, $event)"
|
|
894
|
+
/>
|
|
895
|
+
</div>
|
|
896
|
+
</td>
|
|
897
|
+
</template>
|
|
898
|
+
</ReportComposerCollectionSection>
|
|
899
|
+
|
|
900
|
+
<ReportComposerCollectionSection
|
|
901
|
+
v-if="allowsColumnGroups"
|
|
902
|
+
title="Columns"
|
|
903
|
+
add-label="Add column"
|
|
904
|
+
:items="modelValue.columnGroups"
|
|
905
|
+
:columns="groupingSectionColumns"
|
|
906
|
+
empty-message="No column groups selected. Add one to switch the sheet into pivot mode."
|
|
907
|
+
section="columnGroups"
|
|
908
|
+
:row-key="columnGroupingRowKey"
|
|
909
|
+
@add="addGrouping('columnGroups')"
|
|
910
|
+
@remove="removeGrouping('columnGroups', $event)"
|
|
911
|
+
@dragstart="handleCollectionDragStart"
|
|
912
|
+
@dragover="onComposerDragOver"
|
|
913
|
+
@drop="handleCollectionDrop"
|
|
914
|
+
>
|
|
915
|
+
<template #cells="{ item: group, index }">
|
|
916
|
+
<td class="border-r border-dotted border-ngb-border px-0 py-1 align-top">
|
|
917
|
+
<div class="px-1">
|
|
918
|
+
<NgbSelect
|
|
919
|
+
variant="grid"
|
|
920
|
+
:model-value="group.fieldCode"
|
|
921
|
+
:options="groupFieldOptions"
|
|
922
|
+
@update:model-value="setGroupingField('columnGroups', index, String($event ?? ''))"
|
|
923
|
+
/>
|
|
924
|
+
</div>
|
|
925
|
+
</td>
|
|
926
|
+
<td class="border-r border-dotted border-ngb-border px-0 py-1 align-top">
|
|
927
|
+
<div class="px-1">
|
|
928
|
+
<NgbSelect
|
|
929
|
+
variant="grid"
|
|
930
|
+
:model-value="group.timeGrain ?? ''"
|
|
931
|
+
:options="groupingTimeGrainOptions(group.fieldCode)"
|
|
932
|
+
:disabled="!hasGroupingTimeGrainOptions(group.fieldCode)"
|
|
933
|
+
@update:model-value="setGroupingTimeGrain('columnGroups', index, $event)"
|
|
934
|
+
/>
|
|
935
|
+
</div>
|
|
936
|
+
</td>
|
|
937
|
+
</template>
|
|
938
|
+
</ReportComposerCollectionSection>
|
|
939
|
+
</section>
|
|
940
|
+
|
|
941
|
+
<section v-if="active === 'filters' && hasFiltersTab" class="space-y-4">
|
|
942
|
+
<NgbFormLayout>
|
|
943
|
+
<NgbFormRow
|
|
944
|
+
v-for="field in definition.filters ?? []"
|
|
945
|
+
:key="field.fieldCode"
|
|
946
|
+
:label="field.label"
|
|
947
|
+
:hint="field.description ?? undefined"
|
|
948
|
+
dense
|
|
949
|
+
>
|
|
950
|
+
<FilterFieldControl
|
|
951
|
+
:field="field"
|
|
952
|
+
:state="filterState(field)"
|
|
953
|
+
:lookup-items="lookupItemsByFilterCode[field.fieldCode] ?? []"
|
|
954
|
+
select-empty-label="All"
|
|
955
|
+
:show-open="!!selectedFilterItem(field)"
|
|
956
|
+
:show-clear="!!selectedFilterItem(field)"
|
|
957
|
+
allow-include-descendants
|
|
958
|
+
@lookup-query="emit('filter-query', { fieldCode: field.fieldCode, query: $event })"
|
|
959
|
+
@update:items="setFilterItems(field.fieldCode, $event)"
|
|
960
|
+
@update:raw="setFilterRaw(field.fieldCode, $event)"
|
|
961
|
+
@update:include-descendants="setFilterIncludeDescendants(field.fieldCode, $event)"
|
|
962
|
+
@open="void openFilterItem(field)"
|
|
963
|
+
/>
|
|
964
|
+
</NgbFormRow>
|
|
965
|
+
</NgbFormLayout>
|
|
966
|
+
</section>
|
|
967
|
+
|
|
968
|
+
<section v-if="active === 'fields' && hasFieldsTab" class="space-y-6">
|
|
969
|
+
<ReportComposerCollectionSection
|
|
970
|
+
v-if="allowsMeasures && measureOptions.length > 0"
|
|
971
|
+
title="Measures"
|
|
972
|
+
add-label="Add measure"
|
|
973
|
+
:items="modelValue.measures"
|
|
974
|
+
:columns="measureSectionColumns"
|
|
975
|
+
empty-message="No measures selected."
|
|
976
|
+
section="measures"
|
|
977
|
+
:row-key="measureRowKey"
|
|
978
|
+
@add="addMeasure"
|
|
979
|
+
@remove="removeMeasure($event)"
|
|
980
|
+
@dragstart="handleCollectionDragStart"
|
|
981
|
+
@dragover="onComposerDragOver"
|
|
982
|
+
@drop="handleCollectionDrop"
|
|
983
|
+
>
|
|
984
|
+
<template #cells="{ item: measure, index }">
|
|
985
|
+
<td class="border-r border-dotted border-ngb-border px-0 py-1 align-top">
|
|
986
|
+
<div class="px-1">
|
|
987
|
+
<NgbSelect
|
|
988
|
+
variant="grid"
|
|
989
|
+
:model-value="measure.measureCode"
|
|
990
|
+
:options="measureOptions"
|
|
991
|
+
@update:model-value="setMeasureCode(index, String($event ?? ''))"
|
|
992
|
+
/>
|
|
993
|
+
</div>
|
|
994
|
+
</td>
|
|
995
|
+
<td class="border-r border-dotted border-ngb-border px-0 py-1 align-top">
|
|
996
|
+
<div class="px-1">
|
|
997
|
+
<NgbSelect
|
|
998
|
+
variant="grid"
|
|
999
|
+
:model-value="measureAggregationValue(measure)"
|
|
1000
|
+
:options="measureAggregationOptions(measure.measureCode)"
|
|
1001
|
+
@update:model-value="setMeasureAggregation(index, $event)"
|
|
1002
|
+
/>
|
|
1003
|
+
</div>
|
|
1004
|
+
</td>
|
|
1005
|
+
<td class="border-r border-dotted border-ngb-border px-0 py-1 align-top">
|
|
1006
|
+
<div class="px-1">
|
|
1007
|
+
<NgbInput
|
|
1008
|
+
variant="grid"
|
|
1009
|
+
:model-value="measureLabelValue(index)"
|
|
1010
|
+
@update:model-value="setMeasureLabel(index, String($event ?? ''))"
|
|
1011
|
+
/>
|
|
1012
|
+
</div>
|
|
1013
|
+
</td>
|
|
1014
|
+
</template>
|
|
1015
|
+
</ReportComposerCollectionSection>
|
|
1016
|
+
|
|
1017
|
+
<ReportComposerCollectionSection
|
|
1018
|
+
v-if="allowsDetailFields && selectableFieldOptions.length > 0"
|
|
1019
|
+
title="Detail fields"
|
|
1020
|
+
add-label="Add field"
|
|
1021
|
+
:items="modelValue.detailFields"
|
|
1022
|
+
:columns="detailFieldSectionColumns"
|
|
1023
|
+
empty-message="No detail fields selected."
|
|
1024
|
+
section="detailFields"
|
|
1025
|
+
:row-key="detailFieldRowKey"
|
|
1026
|
+
@add="addDetailField"
|
|
1027
|
+
@remove="removeDetailField($event)"
|
|
1028
|
+
@dragstart="handleCollectionDragStart"
|
|
1029
|
+
@dragover="onComposerDragOver"
|
|
1030
|
+
@drop="handleCollectionDrop"
|
|
1031
|
+
>
|
|
1032
|
+
<template #cells="{ item: fieldCode, index }">
|
|
1033
|
+
<td class="border-r border-dotted border-ngb-border px-0 py-1 align-top">
|
|
1034
|
+
<div class="px-1">
|
|
1035
|
+
<NgbSelect
|
|
1036
|
+
variant="grid"
|
|
1037
|
+
:model-value="fieldCode"
|
|
1038
|
+
:options="detailFieldOptionsFor(fieldCode)"
|
|
1039
|
+
@update:model-value="setDetailField(index, String($event ?? ''))"
|
|
1040
|
+
/>
|
|
1041
|
+
</div>
|
|
1042
|
+
</td>
|
|
1043
|
+
</template>
|
|
1044
|
+
</ReportComposerCollectionSection>
|
|
1045
|
+
</section>
|
|
1046
|
+
|
|
1047
|
+
<section v-if="active === 'sorting' && hasSortingTab" class="space-y-4">
|
|
1048
|
+
<ReportComposerCollectionSection
|
|
1049
|
+
title="Sorting"
|
|
1050
|
+
add-label="Add sort"
|
|
1051
|
+
:items="modelValue.sorts"
|
|
1052
|
+
:columns="sortingSectionColumns"
|
|
1053
|
+
empty-message="No sorting selected."
|
|
1054
|
+
section="sorts"
|
|
1055
|
+
:row-key="sortRowKey"
|
|
1056
|
+
:add-disabled="availableSortTargets.length === 0"
|
|
1057
|
+
table-class="min-w-[520px] w-full table-fixed text-sm"
|
|
1058
|
+
@add="addSort"
|
|
1059
|
+
@remove="removeSort($event)"
|
|
1060
|
+
@dragstart="handleCollectionDragStart"
|
|
1061
|
+
@dragover="onComposerDragOver"
|
|
1062
|
+
@drop="handleCollectionDrop"
|
|
1063
|
+
>
|
|
1064
|
+
<template #cells="{ item: sort, index }">
|
|
1065
|
+
<td class="border-r border-dotted border-ngb-border px-0 py-1 align-top">
|
|
1066
|
+
<div class="px-1">
|
|
1067
|
+
<NgbSelect
|
|
1068
|
+
variant="grid"
|
|
1069
|
+
:model-value="sort.fieldCode"
|
|
1070
|
+
:options="sortFieldOptionsFor(sort.fieldCode)"
|
|
1071
|
+
@update:model-value="setSortField(index, String($event ?? ''))"
|
|
1072
|
+
/>
|
|
1073
|
+
</div>
|
|
1074
|
+
</td>
|
|
1075
|
+
<td class="border-r border-dotted border-ngb-border px-0 py-1 align-top">
|
|
1076
|
+
<div class="px-1">
|
|
1077
|
+
<NgbSelect
|
|
1078
|
+
variant="grid"
|
|
1079
|
+
:model-value="resolveSortAxis(sort)"
|
|
1080
|
+
:options="sortAxisOptionsForField(sort.fieldCode)"
|
|
1081
|
+
@update:model-value="setSortAxis(index, String($event ?? 'row'))"
|
|
1082
|
+
/>
|
|
1083
|
+
</div>
|
|
1084
|
+
</td>
|
|
1085
|
+
<td class="border-r border-dotted border-ngb-border px-0 py-1 align-top">
|
|
1086
|
+
<div class="px-1">
|
|
1087
|
+
<NgbSelect
|
|
1088
|
+
variant="grid"
|
|
1089
|
+
:model-value="sort.direction"
|
|
1090
|
+
:options="sortDirections"
|
|
1091
|
+
@update:model-value="setSortDirection(index, $event)"
|
|
1092
|
+
/>
|
|
1093
|
+
</div>
|
|
1094
|
+
</td>
|
|
1095
|
+
<td class="border-r border-dotted border-ngb-border px-0 py-1 align-top">
|
|
1096
|
+
<div class="px-1">
|
|
1097
|
+
<NgbSelect
|
|
1098
|
+
variant="grid"
|
|
1099
|
+
:model-value="sort.timeGrain ?? ''"
|
|
1100
|
+
:options="sortTimeGrainOptionsForFieldAndAxis(sort.fieldCode, resolveSortAxis(sort))"
|
|
1101
|
+
:disabled="sortTimeGrainOptionsForFieldAndAxis(sort.fieldCode, resolveSortAxis(sort)).length === 0 || isSortTimeGrainLocked(sort.fieldCode, resolveSortAxis(sort))"
|
|
1102
|
+
@update:model-value="setSortTimeGrain(index, $event)"
|
|
1103
|
+
/>
|
|
1104
|
+
</div>
|
|
1105
|
+
</td>
|
|
1106
|
+
</template>
|
|
1107
|
+
</ReportComposerCollectionSection>
|
|
1108
|
+
|
|
1109
|
+
<div v-if="sortingHelpLines.length > 0" class="space-y-1 text-xs text-ngb-muted">
|
|
1110
|
+
<p v-for="line in sortingHelpLines" :key="line">{{ line }}</p>
|
|
1111
|
+
</div>
|
|
1112
|
+
</section>
|
|
1113
|
+
|
|
1114
|
+
<section v-if="active === 'variant' && hasVariantTab" class="space-y-4">
|
|
1115
|
+
<div class="space-y-3">
|
|
1116
|
+
<div class="flex items-center justify-end gap-2">
|
|
1117
|
+
<button
|
|
1118
|
+
type="button"
|
|
1119
|
+
class="ngb-iconbtn"
|
|
1120
|
+
:disabled="variantDisabled || createVariantDisabled || running"
|
|
1121
|
+
title="Create variant"
|
|
1122
|
+
@click="emit('create-variant')"
|
|
1123
|
+
>
|
|
1124
|
+
<NgbIcon name="plus" />
|
|
1125
|
+
</button>
|
|
1126
|
+
<button
|
|
1127
|
+
type="button"
|
|
1128
|
+
class="ngb-iconbtn"
|
|
1129
|
+
:disabled="variantDisabled || editVariantDisabled || running"
|
|
1130
|
+
title="Edit variant"
|
|
1131
|
+
@click="emit('edit-variant')"
|
|
1132
|
+
>
|
|
1133
|
+
<NgbIcon name="edit" />
|
|
1134
|
+
</button>
|
|
1135
|
+
<button
|
|
1136
|
+
type="button"
|
|
1137
|
+
class="ngb-iconbtn"
|
|
1138
|
+
:disabled="variantDisabled || saveVariantDisabled || running"
|
|
1139
|
+
title="Save variant"
|
|
1140
|
+
@click="emit('save-variant')"
|
|
1141
|
+
>
|
|
1142
|
+
<NgbIcon name="save" />
|
|
1143
|
+
</button>
|
|
1144
|
+
<button
|
|
1145
|
+
type="button"
|
|
1146
|
+
class="ngb-iconbtn"
|
|
1147
|
+
:disabled="variantDisabled || deleteVariantDisabled || running"
|
|
1148
|
+
title="Delete variant"
|
|
1149
|
+
@click="emit('delete-variant')"
|
|
1150
|
+
>
|
|
1151
|
+
<NgbIcon name="trash" />
|
|
1152
|
+
</button>
|
|
1153
|
+
<button
|
|
1154
|
+
type="button"
|
|
1155
|
+
class="ngb-iconbtn"
|
|
1156
|
+
:disabled="variantDisabled || resetVariantDisabled || running"
|
|
1157
|
+
title="Reset variant"
|
|
1158
|
+
@click="emit('reset-variant')"
|
|
1159
|
+
>
|
|
1160
|
+
<NgbIcon name="undo" />
|
|
1161
|
+
</button>
|
|
1162
|
+
<button
|
|
1163
|
+
type="button"
|
|
1164
|
+
class="ngb-iconbtn"
|
|
1165
|
+
:disabled="variantDisabled || loadVariantDisabled || running"
|
|
1166
|
+
title="Load variant"
|
|
1167
|
+
@click="emit('load-variant')"
|
|
1168
|
+
>
|
|
1169
|
+
<NgbIcon name="load-variant" />
|
|
1170
|
+
</button>
|
|
1171
|
+
</div>
|
|
1172
|
+
|
|
1173
|
+
<NgbSelect
|
|
1174
|
+
:model-value="selectedVariantCode ?? ''"
|
|
1175
|
+
:options="variantOptions ?? []"
|
|
1176
|
+
@update:model-value="emit('update:selectedVariantCode', String($event ?? ''))"
|
|
1177
|
+
/>
|
|
1178
|
+
|
|
1179
|
+
<div class="text-sm text-ngb-muted">{{ variantSummary ?? 'Using the report definition default layout and filters.' }}</div>
|
|
1180
|
+
</div>
|
|
1181
|
+
</section>
|
|
1182
|
+
</div>
|
|
1183
|
+
</template>
|
|
1184
|
+
</NgbTabs>
|
|
1185
|
+
</div>
|
|
1186
|
+
</div>
|
|
1187
|
+
</template>
|