@ngbplatform/ui 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +44 -0
  3. package/package.json +61 -0
  4. package/public/favicon.svg +6 -0
  5. package/public/silent-check-sso.html +8 -0
  6. package/src/assets/ngb_logo.svg +17 -0
  7. package/src/assets/ngb_logo_color.svg +17 -0
  8. package/src/index.ts +1023 -0
  9. package/src/ngb/README.md +27 -0
  10. package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
  11. package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
  12. package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
  13. package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
  14. package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
  15. package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
  16. package/src/ngb/accounting/api.ts +79 -0
  17. package/src/ngb/accounting/generalJournalEntry.ts +127 -0
  18. package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
  19. package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
  20. package/src/ngb/accounting/navigation.ts +83 -0
  21. package/src/ngb/accounting/periodClosing.ts +75 -0
  22. package/src/ngb/accounting/periodClosingApi.ts +60 -0
  23. package/src/ngb/accounting/periodClosingTypes.ts +73 -0
  24. package/src/ngb/accounting/types.ts +57 -0
  25. package/src/ngb/api/audit.ts +23 -0
  26. package/src/ngb/api/catalogs.ts +50 -0
  27. package/src/ngb/api/contracts.ts +285 -0
  28. package/src/ngb/api/documents.ts +170 -0
  29. package/src/ngb/api/http.ts +446 -0
  30. package/src/ngb/api/lookups.ts +14 -0
  31. package/src/ngb/api/types.ts +6 -0
  32. package/src/ngb/auth/index.ts +13 -0
  33. package/src/ngb/auth/keycloak.ts +374 -0
  34. package/src/ngb/auth/router.ts +35 -0
  35. package/src/ngb/auth/types.ts +12 -0
  36. package/src/ngb/auth/useAuthStore.ts +209 -0
  37. package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
  38. package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
  39. package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
  40. package/src/ngb/command-palette/api.ts +10 -0
  41. package/src/ngb/command-palette/config.ts +16 -0
  42. package/src/ngb/command-palette/presentation.ts +85 -0
  43. package/src/ngb/command-palette/search.ts +130 -0
  44. package/src/ngb/command-palette/staticItems.ts +243 -0
  45. package/src/ngb/command-palette/storage.ts +45 -0
  46. package/src/ngb/command-palette/store.ts +716 -0
  47. package/src/ngb/command-palette/types.ts +120 -0
  48. package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
  49. package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
  50. package/src/ngb/components/NgbConfirmDialog.vue +63 -0
  51. package/src/ngb/components/NgbDialog.vue +44 -0
  52. package/src/ngb/components/NgbDrawer.vue +156 -0
  53. package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
  54. package/src/ngb/components/NgbModalShell.vue +99 -0
  55. package/src/ngb/components/NgbToolbar.vue +14 -0
  56. package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
  57. package/src/ngb/components/forms/NgbFormRow.vue +21 -0
  58. package/src/ngb/components/forms/NgbFormSection.vue +16 -0
  59. package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
  60. package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
  61. package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
  62. package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
  63. package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
  64. package/src/ngb/components/register/registerTypes.ts +51 -0
  65. package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
  66. package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
  67. package/src/ngb/components/register/useRegisterRows.ts +209 -0
  68. package/src/ngb/components/register/useRegisterViewport.ts +56 -0
  69. package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
  70. package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
  71. package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
  72. package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
  73. package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
  74. package/src/ngb/editor/NgbEntityEditor.vue +250 -0
  75. package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
  76. package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
  77. package/src/ngb/editor/catalogNavigation.ts +13 -0
  78. package/src/ngb/editor/config.ts +252 -0
  79. package/src/ngb/editor/documentCopyDraft.ts +245 -0
  80. package/src/ngb/editor/documentEffects.ts +43 -0
  81. package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
  82. package/src/ngb/editor/documentNavigation.ts +115 -0
  83. package/src/ngb/editor/documentStatus.ts +45 -0
  84. package/src/ngb/editor/entityEditorErrors.ts +162 -0
  85. package/src/ngb/editor/entityEditorPersistence.ts +241 -0
  86. package/src/ngb/editor/extensions.ts +40 -0
  87. package/src/ngb/editor/types.ts +295 -0
  88. package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
  89. package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
  90. package/src/ngb/editor/useEditorDrawerState.ts +88 -0
  91. package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
  92. package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
  93. package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
  94. package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
  95. package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
  96. package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
  97. package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
  98. package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
  99. package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
  100. package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
  101. package/src/ngb/env/runtimeConfig.ts +25 -0
  102. package/src/ngb/lookup/config.ts +42 -0
  103. package/src/ngb/lookup/defaultConfig.ts +136 -0
  104. package/src/ngb/lookup/navigation.ts +76 -0
  105. package/src/ngb/lookup/prefetch.ts +40 -0
  106. package/src/ngb/lookup/store.ts +261 -0
  107. package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
  108. package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
  109. package/src/ngb/metadata/NgbEntityForm.vue +84 -0
  110. package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
  111. package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
  112. package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
  113. package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
  114. package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
  115. package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
  116. package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
  117. package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
  118. package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
  119. package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
  120. package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
  121. package/src/ngb/metadata/config.ts +28 -0
  122. package/src/ngb/metadata/dataTypes.ts +27 -0
  123. package/src/ngb/metadata/entityForm.ts +114 -0
  124. package/src/ngb/metadata/entityModel.ts +34 -0
  125. package/src/ngb/metadata/entityValueFormatting.ts +83 -0
  126. package/src/ngb/metadata/fieldRendererState.ts +84 -0
  127. package/src/ngb/metadata/filtering.ts +153 -0
  128. package/src/ngb/metadata/lookup.ts +25 -0
  129. package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
  130. package/src/ngb/metadata/normalization.ts +100 -0
  131. package/src/ngb/metadata/referenceHydration.ts +99 -0
  132. package/src/ngb/metadata/register.ts +135 -0
  133. package/src/ngb/metadata/routePages.ts +86 -0
  134. package/src/ngb/metadata/store.ts +35 -0
  135. package/src/ngb/metadata/types.ts +276 -0
  136. package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
  137. package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
  138. package/src/ngb/metadata/useValidationFocus.ts +49 -0
  139. package/src/ngb/primitives/NgbBadge.vue +31 -0
  140. package/src/ngb/primitives/NgbButton.vue +53 -0
  141. package/src/ngb/primitives/NgbCheckbox.vue +33 -0
  142. package/src/ngb/primitives/NgbDatePicker.vue +141 -0
  143. package/src/ngb/primitives/NgbIcon.vue +545 -0
  144. package/src/ngb/primitives/NgbInput.vue +57 -0
  145. package/src/ngb/primitives/NgbLookup.vue +264 -0
  146. package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
  147. package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
  148. package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
  149. package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
  150. package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
  151. package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
  152. package/src/ngb/primitives/NgbSelect.vue +108 -0
  153. package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
  154. package/src/ngb/primitives/NgbSwitch.vue +42 -0
  155. package/src/ngb/primitives/NgbTabs.vue +116 -0
  156. package/src/ngb/primitives/NgbToastHost.vue +49 -0
  157. package/src/ngb/primitives/NgbTooltip.vue +27 -0
  158. package/src/ngb/primitives/iconNames.ts +83 -0
  159. package/src/ngb/primitives/toast.ts +66 -0
  160. package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
  161. package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
  162. package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
  163. package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
  164. package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
  165. package/src/ngb/reporting/NgbReportPage.vue +1124 -0
  166. package/src/ngb/reporting/NgbReportSheet.vue +467 -0
  167. package/src/ngb/reporting/api.ts +35 -0
  168. package/src/ngb/reporting/composer.ts +904 -0
  169. package/src/ngb/reporting/config.ts +120 -0
  170. package/src/ngb/reporting/defaultConfig.ts +15 -0
  171. package/src/ngb/reporting/lookupFilters.ts +43 -0
  172. package/src/ngb/reporting/navigation.ts +181 -0
  173. package/src/ngb/reporting/pageHelpers.ts +73 -0
  174. package/src/ngb/reporting/pageSession.ts +92 -0
  175. package/src/ngb/reporting/paging.ts +66 -0
  176. package/src/ngb/reporting/types.ts +333 -0
  177. package/src/ngb/router/backNavigation.ts +122 -0
  178. package/src/ngb/router/queryParams.ts +129 -0
  179. package/src/ngb/router/queryState.ts +121 -0
  180. package/src/ngb/router/routeAliases.ts +66 -0
  181. package/src/ngb/router/routeParams.ts +15 -0
  182. package/src/ngb/router/shareLink.ts +39 -0
  183. package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
  184. package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
  185. package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
  186. package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
  187. package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
  188. package/src/ngb/security/NgbRolesPage.vue +125 -0
  189. package/src/ngb/security/NgbUserEditorPage.vue +615 -0
  190. package/src/ngb/security/NgbUsersPage.vue +125 -0
  191. package/src/ngb/security/api.ts +91 -0
  192. package/src/ngb/security/audit.ts +21 -0
  193. package/src/ngb/security/permissions.ts +65 -0
  194. package/src/ngb/security/types.ts +157 -0
  195. package/src/ngb/security/useAccessStore.ts +69 -0
  196. package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
  197. package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
  198. package/src/ngb/site/NgbLogo.vue +44 -0
  199. package/src/ngb/site/NgbPageHeader.vue +49 -0
  200. package/src/ngb/site/NgbSiteShell.vue +286 -0
  201. package/src/ngb/site/NgbSiteSidebar.vue +270 -0
  202. package/src/ngb/site/NgbTopBar.vue +425 -0
  203. package/src/ngb/site/NgbTrendChart.vue +224 -0
  204. package/src/ngb/site/dashboardData.ts +324 -0
  205. package/src/ngb/site/mainMenuStore.ts +49 -0
  206. package/src/ngb/site/types.ts +31 -0
  207. package/src/ngb/site/useDashboardPageState.ts +98 -0
  208. package/src/ngb/site/useTheme.ts +95 -0
  209. package/src/ngb/utils/clone.ts +43 -0
  210. package/src/ngb/utils/dateValues.ts +107 -0
  211. package/src/ngb/utils/errorMessage.ts +14 -0
  212. package/src/ngb/utils/guid.ts +21 -0
  213. package/src/ngb/utils/stableValue.ts +21 -0
  214. package/src/ngb/utils/storage.ts +135 -0
  215. package/src/styles/tailwind.css +165 -0
  216. package/src/vite/publicAssets.ts +104 -0
  217. package/vite-public-assets.js +68 -0
@@ -0,0 +1,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
+ }