@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,467 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import { useRouter } from 'vue-router'
|
|
4
|
+
import NgbBadge from '../primitives/NgbBadge.vue'
|
|
5
|
+
|
|
6
|
+
import { resolveReportCellActionUrl } from './config'
|
|
7
|
+
import { ReportRowKind, type ReportCellDto, type ReportSheetDto, type ReportSheetRowDto } from './types'
|
|
8
|
+
import type { ReportRouteContext, ReportSourceTrail } from './navigation'
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
sheet: ReportSheetDto | null
|
|
12
|
+
loading?: boolean
|
|
13
|
+
loadingMore?: boolean
|
|
14
|
+
canLoadMore?: boolean
|
|
15
|
+
showEndOfList?: boolean
|
|
16
|
+
loadedCount?: number | null
|
|
17
|
+
totalCount?: number | null
|
|
18
|
+
rowNoun?: string | null
|
|
19
|
+
emptyTitle?: string
|
|
20
|
+
emptyMessage?: string
|
|
21
|
+
currentReportContext?: ReportRouteContext | null
|
|
22
|
+
sourceTrail?: ReportSourceTrail | null
|
|
23
|
+
backTarget?: string | null
|
|
24
|
+
}>()
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits<{
|
|
27
|
+
(e: 'load-more'): void
|
|
28
|
+
(e: 'scroll-top-change', value: number): void
|
|
29
|
+
}>()
|
|
30
|
+
|
|
31
|
+
const hasRows = computed(() => (props.sheet?.rows?.length ?? 0) > 0)
|
|
32
|
+
const headerRows = computed(() => props.sheet?.headerRows ?? [])
|
|
33
|
+
const hasColumnGroups = computed(() => headerRows.value.length > 0)
|
|
34
|
+
const rowAxisColumnCount = computed(() => {
|
|
35
|
+
if (!hasColumnGroups.value) return 0
|
|
36
|
+
|
|
37
|
+
const firstHeaderRow = headerRows.value[0]
|
|
38
|
+
if (!firstHeaderRow) return 0
|
|
39
|
+
|
|
40
|
+
const headerDepth = headerRows.value.length
|
|
41
|
+
let count = 0
|
|
42
|
+
for (const cell of firstHeaderRow.cells) {
|
|
43
|
+
if ((cell.rowSpan ?? 1) !== headerDepth) break
|
|
44
|
+
count += 1
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return count
|
|
48
|
+
})
|
|
49
|
+
const totalColumnStartIndex = computed(() => {
|
|
50
|
+
if (!hasColumnGroups.value) return -1
|
|
51
|
+
return props.sheet?.columns?.findIndex(column => column.semanticRole === 'pivot-total') ?? -1
|
|
52
|
+
})
|
|
53
|
+
const totalMeasureColumnCount = computed(() => {
|
|
54
|
+
if (!hasColumnGroups.value) return 0
|
|
55
|
+
return props.sheet?.columns?.filter(column => column.semanticRole === 'pivot-total').length ?? 0
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const router = useRouter()
|
|
59
|
+
const decimalFormatter = new Intl.NumberFormat(undefined, {
|
|
60
|
+
minimumFractionDigits: 2,
|
|
61
|
+
maximumFractionDigits: 2,
|
|
62
|
+
})
|
|
63
|
+
const integerFormatter = new Intl.NumberFormat(undefined, {
|
|
64
|
+
maximumFractionDigits: 0,
|
|
65
|
+
})
|
|
66
|
+
const scrollHost = ref<HTMLDivElement | null>(null)
|
|
67
|
+
const loadMoreSentinel = ref<HTMLDivElement | null>(null)
|
|
68
|
+
let loadMoreObserver: IntersectionObserver | null = null
|
|
69
|
+
let loadMoreRequestPending = false
|
|
70
|
+
|
|
71
|
+
function drilldownRoute(cell: ReportCellDto): string | null {
|
|
72
|
+
return resolveReportCellActionUrl(cell.action, {
|
|
73
|
+
currentReportContext: props.currentReportContext ?? null,
|
|
74
|
+
sourceTrail: props.sourceTrail ?? null,
|
|
75
|
+
backTarget: props.backTarget ?? null,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function onCellActivate(cell: ReportCellDto) {
|
|
80
|
+
const to = drilldownRoute(cell)
|
|
81
|
+
if (!to) return
|
|
82
|
+
await router.push(to)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function rowKindLabel(row: ReportSheetRowDto): string | null {
|
|
86
|
+
switch (row.rowKind) {
|
|
87
|
+
case ReportRowKind.Group: return 'Group'
|
|
88
|
+
case ReportRowKind.Subtotal: return 'Subtotal'
|
|
89
|
+
case ReportRowKind.Total: return 'Total'
|
|
90
|
+
default: return null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function rowClass(row: ReportSheetRowDto): string {
|
|
95
|
+
if (hasColumnGroups.value) {
|
|
96
|
+
switch (row.rowKind) {
|
|
97
|
+
case ReportRowKind.Group:
|
|
98
|
+
return 'bg-ngb-card font-medium'
|
|
99
|
+
case ReportRowKind.Subtotal:
|
|
100
|
+
return 'bg-[rgba(11,60,93,.04)] font-medium'
|
|
101
|
+
case ReportRowKind.Total:
|
|
102
|
+
return 'bg-[rgba(11,60,93,.08)] font-semibold'
|
|
103
|
+
default:
|
|
104
|
+
return 'bg-ngb-card'
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
switch (row.rowKind) {
|
|
109
|
+
case ReportRowKind.Group:
|
|
110
|
+
return 'bg-[var(--ngb-row-hover)] font-medium'
|
|
111
|
+
case ReportRowKind.Subtotal:
|
|
112
|
+
return 'bg-[rgba(11,60,93,.06)] font-medium'
|
|
113
|
+
case ReportRowKind.Total:
|
|
114
|
+
return 'bg-[rgba(11,60,93,.10)] font-semibold'
|
|
115
|
+
default:
|
|
116
|
+
return 'bg-ngb-card'
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isSubtotalOrTotal(row: ReportSheetRowDto): boolean {
|
|
121
|
+
return row.rowKind === ReportRowKind.Subtotal
|
|
122
|
+
|| row.rowKind === ReportRowKind.Total
|
|
123
|
+
|| row.rowKind === 'Subtotal'
|
|
124
|
+
|| row.rowKind === 'Total'
|
|
125
|
+
|| row.rowKind === 'subtotal'
|
|
126
|
+
|| row.rowKind === 'total'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalizeValueType(valueType?: string | null): string {
|
|
130
|
+
return String(valueType ?? '').trim().toLowerCase()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isDecimalValueType(valueType?: string | null): boolean {
|
|
134
|
+
const normalized = normalizeValueType(valueType)
|
|
135
|
+
return normalized === 'decimal' || normalized === 'double' || normalized === 'float' || normalized === 'single'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeCount(value: unknown): number | null {
|
|
139
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return null
|
|
140
|
+
return Math.floor(value)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeRowNoun(value: string | null | undefined): string {
|
|
144
|
+
const normalized = String(value ?? '').trim().toLowerCase()
|
|
145
|
+
return normalized.length > 0 ? normalized : 'row'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function pluralizeRowNoun(count: number, noun: string): string {
|
|
149
|
+
if (count === 1) return noun
|
|
150
|
+
if (noun.endsWith('s')) return noun
|
|
151
|
+
if (/[^aeiou]y$/i.test(noun)) return `${noun.slice(0, -1)}ies`
|
|
152
|
+
return `${noun}s`
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatCountWithRowNoun(count: number): string {
|
|
156
|
+
const noun = pluralizeRowNoun(count, normalizeRowNoun(props.rowNoun))
|
|
157
|
+
return `${integerFormatter.format(count)} ${noun}`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const footerStatusText = computed(() => {
|
|
161
|
+
const loadedCount = normalizeCount(props.loadedCount) ?? (props.sheet?.rows?.length ?? 0)
|
|
162
|
+
const totalCount = normalizeCount(props.totalCount)
|
|
163
|
+
|
|
164
|
+
if (props.loadingMore) return `Loading more ${pluralizeRowNoun(Math.max(loadedCount, 2), normalizeRowNoun(props.rowNoun))}…`
|
|
165
|
+
if (props.canLoadMore) return `Loaded ${formatCountWithRowNoun(loadedCount)}. Scroll to continue loading.`
|
|
166
|
+
if (props.showEndOfList) {
|
|
167
|
+
if (totalCount != null && totalCount >= loadedCount) {
|
|
168
|
+
return `Loaded ${formatCountWithRowNoun(totalCount)}. End of list.`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return `Loaded ${formatCountWithRowNoun(loadedCount)}. End of list.`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return null
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
function tryFormatDecimal(value: unknown): string | null {
|
|
178
|
+
if (typeof value === 'number' && Number.isFinite(value)) return decimalFormatter.format(value)
|
|
179
|
+
if (typeof value === 'string') {
|
|
180
|
+
const trimmed = value.trim()
|
|
181
|
+
if (trimmed.length === 0) return null
|
|
182
|
+
const normalized = trimmed.replace(/,/g, '')
|
|
183
|
+
const parsed = Number(normalized)
|
|
184
|
+
if (Number.isFinite(parsed)) return decimalFormatter.format(parsed)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function cellText(cell: ReportCellDto): string {
|
|
191
|
+
if (isDecimalValueType(cell.valueType)) {
|
|
192
|
+
const formattedValue = tryFormatDecimal(cell.value)
|
|
193
|
+
if (formattedValue != null) return formattedValue
|
|
194
|
+
|
|
195
|
+
const formattedDisplay = tryFormatDecimal(cell.display)
|
|
196
|
+
if (formattedDisplay != null) return formattedDisplay
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (cell.display != null && String(cell.display).trim().length > 0) return String(cell.display)
|
|
200
|
+
if (cell.value == null) return ''
|
|
201
|
+
if (typeof cell.value === 'string') return cell.value
|
|
202
|
+
if (typeof cell.value === 'number' || typeof cell.value === 'boolean') return String(cell.value)
|
|
203
|
+
return JSON.stringify(cell.value)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function headerCellStyle(rowIndex: number) {
|
|
207
|
+
return {
|
|
208
|
+
position: 'sticky',
|
|
209
|
+
top: `${rowIndex * 49}px`,
|
|
210
|
+
zIndex: 10,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function rowRenderKey(row: ReportSheetRowDto, rowIndex: number): string {
|
|
215
|
+
return `${String(row.rowKind ?? 'row')}:${String(row.groupKey ?? 'nogroup')}:${rowIndex}`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function headerCellClass(cell: ReportCellDto, headerIndex: number, cellIndex: number): string {
|
|
219
|
+
const classes = ['border-b', 'border-ngb-border', 'text-left', 'leading-snug', 'whitespace-pre-wrap', 'break-words']
|
|
220
|
+
|
|
221
|
+
if (!hasColumnGroups.value) {
|
|
222
|
+
classes.push('bg-ngb-card', 'px-4', 'py-3', 'font-semibold', 'text-ngb-text')
|
|
223
|
+
return classes.join(' ')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
classes.push('bg-ngb-card', 'px-4')
|
|
227
|
+
|
|
228
|
+
if (headerIndex === headerRows.value.length - 1) {
|
|
229
|
+
classes.push('py-3', 'font-semibold', 'text-ngb-text')
|
|
230
|
+
} else {
|
|
231
|
+
classes.push('py-2.5', 'font-medium', 'text-ngb-muted')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (headerIndex === 0 && cellIndex === rowAxisColumnCount.value - 1 && rowAxisColumnCount.value > 0)
|
|
235
|
+
classes.push('border-r-2')
|
|
236
|
+
|
|
237
|
+
if (cellText(cell) === 'Total')
|
|
238
|
+
classes.push('border-l-2', 'text-ngb-text')
|
|
239
|
+
|
|
240
|
+
if (isTotalLeafHeaderCell(headerIndex, cellIndex))
|
|
241
|
+
classes.push('border-l-2')
|
|
242
|
+
|
|
243
|
+
return classes.join(' ')
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function bodyCellClass(row: ReportSheetRowDto, cellIndex: number): string {
|
|
247
|
+
const classes = ['border-b', 'border-ngb-border/70', 'px-4', 'py-3', 'align-top', 'text-ngb-text']
|
|
248
|
+
|
|
249
|
+
if (isSubtotalOrTotal(row))
|
|
250
|
+
classes.push('font-semibold')
|
|
251
|
+
|
|
252
|
+
if (hasColumnGroups.value) {
|
|
253
|
+
if (cellIndex === rowAxisColumnCount.value - 1 && rowAxisColumnCount.value > 0)
|
|
254
|
+
classes.push('border-r-2')
|
|
255
|
+
|
|
256
|
+
if (totalColumnStartIndex.value >= 0 && cellIndex === totalColumnStartIndex.value)
|
|
257
|
+
classes.push('border-l-2')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return classes.join(' ')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function tableClass(): string {
|
|
264
|
+
return hasColumnGroups.value
|
|
265
|
+
? 'min-w-full border-collapse text-sm'
|
|
266
|
+
: 'min-w-full border-collapse text-sm'
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function bodyRowHoverClass(): string {
|
|
270
|
+
return hasColumnGroups.value ? 'hover:bg-[rgba(11,60,93,.025)]' : ''
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function bodyContentClass(cellIndex: number): string {
|
|
274
|
+
if (hasColumnGroups.value && cellIndex === 0) return 'flex items-start gap-2 min-w-[26rem]'
|
|
275
|
+
return 'flex items-start gap-2'
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isTotalLeafHeaderCell(headerIndex: number, cellIndex: number): boolean {
|
|
279
|
+
if (!hasColumnGroups.value) return false
|
|
280
|
+
if (headerIndex !== headerRows.value.length - 1) return false
|
|
281
|
+
if (totalMeasureColumnCount.value <= 0) return false
|
|
282
|
+
|
|
283
|
+
const firstTotalHeaderIndex = headerRows.value[headerIndex].cells.length - totalMeasureColumnCount.value
|
|
284
|
+
return cellIndex === firstTotalHeaderIndex
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function shouldEmitLoadMore(): boolean {
|
|
288
|
+
return !!props.canLoadMore && !props.loadingMore && !props.loading && hasRows.value
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function requestLoadMore() {
|
|
292
|
+
if (loadMoreRequestPending) return
|
|
293
|
+
if (!shouldEmitLoadMore()) return
|
|
294
|
+
loadMoreRequestPending = true
|
|
295
|
+
emit('load-more')
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function disconnectLoadMoreObserver() {
|
|
299
|
+
loadMoreObserver?.disconnect()
|
|
300
|
+
loadMoreObserver = null
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function syncLoadMoreObserver() {
|
|
304
|
+
disconnectLoadMoreObserver()
|
|
305
|
+
|
|
306
|
+
if (typeof IntersectionObserver === 'undefined') return
|
|
307
|
+
if (!shouldEmitLoadMore()) return
|
|
308
|
+
if (!scrollHost.value || !loadMoreSentinel.value) return
|
|
309
|
+
|
|
310
|
+
loadMoreObserver = new IntersectionObserver((entries) => {
|
|
311
|
+
if (entries.some((entry) => entry.isIntersecting)) requestLoadMore()
|
|
312
|
+
}, {
|
|
313
|
+
root: scrollHost.value,
|
|
314
|
+
rootMargin: '0px 0px 320px 0px',
|
|
315
|
+
threshold: 0.01,
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
loadMoreObserver.observe(loadMoreSentinel.value)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function onScroll() {
|
|
322
|
+
emit('scroll-top-change', scrollHost.value?.scrollTop ?? 0)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function restoreScrollTop(value: number) {
|
|
326
|
+
if (!scrollHost.value) return
|
|
327
|
+
scrollHost.value.scrollTop = Math.max(0, Math.floor(value))
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
defineExpose({
|
|
331
|
+
restoreScrollTop,
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
watch(
|
|
335
|
+
() => [props.canLoadMore, props.loadingMore, props.loading, hasRows.value, props.sheet, props.sheet?.rows?.length ?? 0],
|
|
336
|
+
async () => {
|
|
337
|
+
loadMoreRequestPending = false
|
|
338
|
+
await nextTick()
|
|
339
|
+
syncLoadMoreObserver()
|
|
340
|
+
},
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
onMounted(async () => {
|
|
344
|
+
loadMoreRequestPending = false
|
|
345
|
+
await nextTick()
|
|
346
|
+
syncLoadMoreObserver()
|
|
347
|
+
emit('scroll-top-change', scrollHost.value?.scrollTop ?? 0)
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
onBeforeUnmount(() => {
|
|
351
|
+
loadMoreRequestPending = false
|
|
352
|
+
disconnectLoadMoreObserver()
|
|
353
|
+
})
|
|
354
|
+
</script>
|
|
355
|
+
|
|
356
|
+
<template>
|
|
357
|
+
<div class="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
358
|
+
<div
|
|
359
|
+
v-if="loading && !hasRows"
|
|
360
|
+
data-testid="report-sheet-loading"
|
|
361
|
+
class="rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-5 shadow-card"
|
|
362
|
+
>
|
|
363
|
+
<div class="text-sm font-semibold text-ngb-text">Running report…</div>
|
|
364
|
+
<div class="mt-2 text-sm text-ngb-muted">The Report Composer is materializing the first sheet for the selected layout.</div>
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
<div
|
|
368
|
+
v-else-if="!hasRows"
|
|
369
|
+
data-testid="report-sheet-empty"
|
|
370
|
+
class="rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-5 shadow-card"
|
|
371
|
+
>
|
|
372
|
+
<div class="text-sm font-semibold text-ngb-text">{{ emptyTitle ?? 'No rows for this layout' }}</div>
|
|
373
|
+
<div class="mt-2 text-sm text-ngb-muted">{{ emptyMessage ?? 'Adjust filters, grouping, or measures and run the report again.' }}</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div
|
|
377
|
+
v-else
|
|
378
|
+
ref="scrollHost"
|
|
379
|
+
data-testid="report-sheet-scroll"
|
|
380
|
+
class="min-h-0 min-w-0 overflow-auto overscroll-contain rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card shadow-card"
|
|
381
|
+
@scroll="onScroll"
|
|
382
|
+
>
|
|
383
|
+
<table :class="tableClass()" data-testid="report-sheet-table">
|
|
384
|
+
<thead class="bg-ngb-card">
|
|
385
|
+
<template v-if="headerRows.length > 0">
|
|
386
|
+
<tr v-for="(headerRow, headerIndex) in headerRows" :key="headerRow.groupKey ?? `header:${headerIndex}`">
|
|
387
|
+
<th
|
|
388
|
+
v-for="(cell, cellIndex) in headerRow.cells"
|
|
389
|
+
:key="`${headerIndex}:${cellIndex}`"
|
|
390
|
+
:class="headerCellClass(cell, headerIndex, cellIndex)"
|
|
391
|
+
:colspan="cell.colSpan ?? 1"
|
|
392
|
+
:rowspan="cell.rowSpan ?? 1"
|
|
393
|
+
:style="headerCellStyle(headerIndex)"
|
|
394
|
+
>
|
|
395
|
+
<button
|
|
396
|
+
v-if="drilldownRoute(cell)"
|
|
397
|
+
type="button"
|
|
398
|
+
class="cursor-pointer whitespace-pre-wrap break-words text-left hover:underline"
|
|
399
|
+
@click="onCellActivate(cell)"
|
|
400
|
+
>
|
|
401
|
+
{{ cellText(cell) }}
|
|
402
|
+
</button>
|
|
403
|
+
<span v-else>{{ cellText(cell) }}</span>
|
|
404
|
+
</th>
|
|
405
|
+
</tr>
|
|
406
|
+
</template>
|
|
407
|
+
|
|
408
|
+
<tr v-else class="sticky top-0 z-10 bg-ngb-card">
|
|
409
|
+
<th
|
|
410
|
+
v-for="column in sheet?.columns ?? []"
|
|
411
|
+
:key="column.code"
|
|
412
|
+
class="border-b border-ngb-border px-4 py-3 text-left font-semibold leading-snug text-ngb-text whitespace-pre-wrap break-words"
|
|
413
|
+
>
|
|
414
|
+
{{ column.title }}
|
|
415
|
+
</th>
|
|
416
|
+
</tr>
|
|
417
|
+
</thead>
|
|
418
|
+
|
|
419
|
+
<tbody>
|
|
420
|
+
<tr
|
|
421
|
+
v-for="(row, rowIndex) in sheet?.rows ?? []"
|
|
422
|
+
:key="rowRenderKey(row, rowIndex)"
|
|
423
|
+
:class="[rowClass(row), bodyRowHoverClass()]"
|
|
424
|
+
>
|
|
425
|
+
<td
|
|
426
|
+
v-for="(cell, cellIndex) in row.cells"
|
|
427
|
+
:key="`${rowRenderKey(row, rowIndex)}:${cellIndex}`"
|
|
428
|
+
:class="bodyCellClass(row, cellIndex)"
|
|
429
|
+
:colspan="cell.colSpan ?? 1"
|
|
430
|
+
:rowspan="cell.rowSpan ?? 1"
|
|
431
|
+
>
|
|
432
|
+
<div
|
|
433
|
+
:class="bodyContentClass(cellIndex)"
|
|
434
|
+
:style="cellIndex === 0 ? { paddingLeft: `${(row.outlineLevel ?? 0) * 16}px` } : undefined"
|
|
435
|
+
>
|
|
436
|
+
<NgbBadge v-if="cellIndex === 0 && rowKindLabel(row)" tone="neutral">{{ rowKindLabel(row) }}</NgbBadge>
|
|
437
|
+
<button
|
|
438
|
+
v-if="drilldownRoute(cell)"
|
|
439
|
+
type="button"
|
|
440
|
+
class="cursor-pointer whitespace-pre-wrap break-words text-left hover:underline"
|
|
441
|
+
:class="isSubtotalOrTotal(row) ? 'font-semibold' : undefined"
|
|
442
|
+
@click="onCellActivate(cell)"
|
|
443
|
+
>
|
|
444
|
+
{{ cellText(cell) }}
|
|
445
|
+
</button>
|
|
446
|
+
<span v-else class="whitespace-pre-wrap break-words" :class="isSubtotalOrTotal(row) ? 'font-semibold' : undefined">{{ cellText(cell) }}</span>
|
|
447
|
+
</div>
|
|
448
|
+
</td>
|
|
449
|
+
</tr>
|
|
450
|
+
</tbody>
|
|
451
|
+
</table>
|
|
452
|
+
|
|
453
|
+
<div ref="loadMoreSentinel" class="h-px w-full" aria-hidden="true" />
|
|
454
|
+
|
|
455
|
+
<div
|
|
456
|
+
v-if="canLoadMore || loadingMore || showEndOfList"
|
|
457
|
+
class="sticky bottom-0 flex items-center justify-between gap-3 border-t border-ngb-border bg-ngb-card/95 px-4 py-3 backdrop-blur"
|
|
458
|
+
>
|
|
459
|
+
<div class="text-sm text-ngb-muted">{{ footerStatusText }}</div>
|
|
460
|
+
|
|
461
|
+
<button v-if="canLoadMore && !loadingMore" type="button" class="inline-flex items-center justify-center rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card px-3 py-1.5 text-sm font-medium text-ngb-text shadow-card transition hover:bg-[var(--ngb-row-hover)]" @click="requestLoadMore">
|
|
462
|
+
Load more
|
|
463
|
+
</button>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { httpDelete, httpGet, httpPost, httpPostFile, httpPut } from '../api/http'
|
|
2
|
+
import type { ReportDefinitionDto, ReportExecutionRequestDto, ReportExecutionResponseDto, ReportExportRequestDto, ReportVariantDto } from './types'
|
|
3
|
+
|
|
4
|
+
export async function getReportDefinitions(): Promise<ReportDefinitionDto[]> {
|
|
5
|
+
return await httpGet<ReportDefinitionDto[]>('/api/report-definitions')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function getReportDefinition(reportCode: string): Promise<ReportDefinitionDto> {
|
|
9
|
+
return await httpGet<ReportDefinitionDto>(`/api/report-definitions/${encodeURIComponent(reportCode)}`)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function executeReport(reportCode: string, request: ReportExecutionRequestDto): Promise<ReportExecutionResponseDto> {
|
|
13
|
+
return await httpPost<ReportExecutionResponseDto>(`/api/reports/${encodeURIComponent(reportCode)}/execute`, request)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function exportReportXlsx(reportCode: string, request: ReportExportRequestDto): Promise<{ blob: Blob; fileName: string | null }> {
|
|
17
|
+
const response = await httpPostFile(`/api/reports/${encodeURIComponent(reportCode)}/export/xlsx`, request)
|
|
18
|
+
return { blob: response.blob, fileName: response.fileName }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function getReportVariants(reportCode: string): Promise<ReportVariantDto[]> {
|
|
22
|
+
return await httpGet<ReportVariantDto[]>(`/api/reports/${encodeURIComponent(reportCode)}/variants`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function getReportVariant(reportCode: string, variantCode: string): Promise<ReportVariantDto> {
|
|
26
|
+
return await httpGet<ReportVariantDto>(`/api/reports/${encodeURIComponent(reportCode)}/variants/${encodeURIComponent(variantCode)}`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function saveReportVariant(reportCode: string, variantCode: string, variant: ReportVariantDto): Promise<ReportVariantDto> {
|
|
30
|
+
return await httpPut<ReportVariantDto>(`/api/reports/${encodeURIComponent(reportCode)}/variants/${encodeURIComponent(variantCode)}`, variant)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function deleteReportVariant(reportCode: string, variantCode: string): Promise<void> {
|
|
34
|
+
await httpDelete<void>(`/api/reports/${encodeURIComponent(reportCode)}/variants/${encodeURIComponent(variantCode)}`)
|
|
35
|
+
}
|