@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,409 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue';
3
+ import { useRoute, useRouter } from 'vue-router';
4
+
5
+ import NgbBadge from '../primitives/NgbBadge.vue';
6
+ import NgbIcon from '../primitives/NgbIcon.vue';
7
+ import NgbStatusIcon from '../primitives/NgbStatusIcon.vue';
8
+ import { useToasts } from '../primitives/toast';
9
+ import NgbPageHeader from '../site/NgbPageHeader.vue';
10
+ import { copyAppLink } from '../router/shareLink';
11
+ import { currentRouteBackTarget, navigateBack, resolveBackTarget, withBackTarget } from '../router/backNavigation';
12
+ import { toErrorMessage } from '../utils/errorMessage';
13
+
14
+ import { getConfiguredNgbEditor } from './config';
15
+ import { buildDocumentFlowPageUrl, buildDocumentFullPageUrl, resolveDocumentReopenTarget } from './documentNavigation';
16
+ import { documentStatusLabel, documentStatusTone, documentStatusVisual } from './documentStatus';
17
+ import type { RelationshipGraph, RelationshipGraphNode } from './types';
18
+
19
+ type FlowRow = {
20
+ key: string;
21
+ node: RelationshipGraphNode;
22
+ level: number;
23
+ ancestorHasNext: boolean[];
24
+ isCurrent: boolean;
25
+ };
26
+
27
+ const TREE_INDENT_PX = 42;
28
+ const TREE_LINE_LEFT_PX = 18;
29
+ const TREE_ROW_GAP_PX = 18;
30
+ const DEFAULT_DEPTH = 5;
31
+ const DEFAULT_MAX_NODES = 200;
32
+ const FLOW_CODES = new Set(['based_on', 'created_from', 'reversal_of', 'related_to', 'supersedes']);
33
+
34
+ const route = useRoute();
35
+ const router = useRouter();
36
+ const toasts = useToasts();
37
+
38
+ const loading = ref(false);
39
+ const error = ref<string | null>(null);
40
+ const graph = ref<RelationshipGraph | null>(null);
41
+
42
+ const documentType = computed(() => String(route.params.documentType ?? '').trim());
43
+ const documentId = computed(() => String(route.params.id ?? '').trim());
44
+
45
+ const documentRoute = computed(() => {
46
+ if (!documentType.value || !documentId.value) return '/';
47
+ return resolveDocumentReopenTarget(route, documentType.value, documentId.value);
48
+ });
49
+
50
+ const shareRoute = computed(() => {
51
+ if (!documentType.value || !documentId.value) return '/';
52
+ return buildDocumentFlowPageUrl(documentType.value, documentId.value);
53
+ });
54
+
55
+ const rootNode = computed<RelationshipGraphNode | null>(() => {
56
+ const entityId = documentId.value;
57
+ if (!entityId) return graph.value?.nodes?.[0] ?? null;
58
+ return graph.value?.nodes.find((node) => sameId(node.entityId, entityId)) ?? graph.value?.nodes?.[0] ?? null;
59
+ });
60
+
61
+ const title = computed(() => String(rootNode.value?.title ?? '').trim() || 'Document flow');
62
+ const statusTone = computed<'neutral' | 'success' | 'warn'>(() => documentStatusTone(rootNode.value?.documentStatus));
63
+ const statusLabel = computed(() => documentStatusLabel(rootNode.value?.documentStatus));
64
+ const canShareLink = computed(() => !!documentType.value && !!documentId.value);
65
+
66
+ const rootNodeId = computed(() => rootNode.value?.nodeId ?? null);
67
+
68
+ const flowRows = computed<FlowRow[]>(() => {
69
+ const nodes = graph.value?.nodes ?? [];
70
+ const edges = graph.value?.edges ?? [];
71
+ if (nodes.length === 0) return [];
72
+
73
+ const nodeById = new Map(nodes.map((node) => [node.nodeId, node] as const));
74
+ const root = rootNodeId.value ?? nodes[0]?.nodeId ?? null;
75
+ if (!root || !nodeById.has(root)) return [];
76
+
77
+ const adjacency = new Map<string, Set<string>>();
78
+ for (const edge of edges) {
79
+ const relationship = String(edge.relationshipType ?? '').trim().toLowerCase();
80
+ if (!FLOW_CODES.has(relationship)) continue;
81
+ if (!nodeById.has(edge.fromNodeId) || !nodeById.has(edge.toNodeId)) continue;
82
+ if (edge.fromNodeId === edge.toNodeId) continue;
83
+
84
+ const from = adjacency.get(edge.fromNodeId) ?? new Set<string>();
85
+ from.add(edge.toNodeId);
86
+ adjacency.set(edge.fromNodeId, from);
87
+
88
+ const to = adjacency.get(edge.toNodeId) ?? new Set<string>();
89
+ to.add(edge.fromNodeId);
90
+ adjacency.set(edge.toNodeId, to);
91
+ }
92
+
93
+ const component = new Set<string>();
94
+ const queue: string[] = [root];
95
+ while (queue.length > 0) {
96
+ const current = queue.shift()!;
97
+ if (component.has(current)) continue;
98
+ component.add(current);
99
+ for (const next of adjacency.get(current) ?? []) {
100
+ if (!component.has(next)) queue.push(next);
101
+ }
102
+ }
103
+
104
+ if (component.size === 0) component.add(root);
105
+
106
+ const childrenByParent = new Map<string, string[]>();
107
+ const parentByNode = new Map<string, string | null>([[root, null]]);
108
+ const bfsQueue: string[] = [root];
109
+
110
+ while (bfsQueue.length > 0) {
111
+ const current = bfsQueue.shift()!;
112
+ const neighbors = Array.from(adjacency.get(current) ?? [])
113
+ .filter((nodeId) => component.has(nodeId) && !parentByNode.has(nodeId))
114
+ .sort((a, b) => compareNodes(nodeById.get(a), nodeById.get(b)));
115
+
116
+ if (neighbors.length > 0) {
117
+ childrenByParent.set(current, neighbors);
118
+ }
119
+
120
+ for (const neighborId of neighbors) {
121
+ parentByNode.set(neighborId, current);
122
+ bfsQueue.push(neighborId);
123
+ }
124
+ }
125
+
126
+ const rows: FlowRow[] = [];
127
+ const walk = (nodeId: string, ancestorHasNext: boolean[]) => {
128
+ const node = nodeById.get(nodeId);
129
+ if (!node) return;
130
+
131
+ rows.push({
132
+ key: `${nodeId}:${rows.length}`,
133
+ node,
134
+ level: ancestorHasNext.length,
135
+ ancestorHasNext,
136
+ isCurrent: nodeId === root,
137
+ });
138
+
139
+ const children = childrenByParent.get(nodeId) ?? [];
140
+ children.forEach((childId, index) => {
141
+ walk(childId, [...ancestorHasNext, index < children.length - 1]);
142
+ });
143
+ };
144
+
145
+ walk(root, []);
146
+ return rows;
147
+ });
148
+
149
+ function sameId(a: string | null | undefined, b: string | null | undefined): boolean {
150
+ return String(a ?? '').trim().toLowerCase() === String(b ?? '').trim().toLowerCase();
151
+ }
152
+
153
+ function compareNodes(a?: RelationshipGraphNode, b?: RelationshipGraphNode): number {
154
+ const aDate = sortableDate(a?.subtitle);
155
+ const bDate = sortableDate(b?.subtitle);
156
+ if (aDate !== bDate) return aDate.localeCompare(bDate);
157
+
158
+ const aDisplay = displayForNode(a).toLowerCase();
159
+ const bDisplay = displayForNode(b).toLowerCase();
160
+ return aDisplay.localeCompare(bDisplay);
161
+ }
162
+
163
+ function sortableDate(value: string | null | undefined): string {
164
+ const raw = String(value ?? '').trim();
165
+ if (!raw) return '9999-99-99';
166
+ const date = new Date(raw);
167
+ if (Number.isNaN(date.getTime())) return raw;
168
+ return date.toISOString();
169
+ }
170
+
171
+ function statusIconStatus(node: RelationshipGraphNode): 'saved' | 'posted' | 'marked' {
172
+ return documentStatusVisual(node.documentStatus);
173
+ }
174
+
175
+ function displayForNode(node: RelationshipGraphNode | null | undefined): string {
176
+ const titleValue = String(node?.title ?? '').trim();
177
+ if (titleValue) return titleValue;
178
+ return String(node?.entityId ?? '').trim();
179
+ }
180
+
181
+ function formatMoney(value: number | null | undefined): string {
182
+ if (value == null || !Number.isFinite(value)) return '';
183
+ return value.toLocaleString(undefined, {
184
+ minimumFractionDigits: 2,
185
+ maximumFractionDigits: 2,
186
+ });
187
+ }
188
+
189
+ function amountLabel(node: RelationshipGraphNode): string {
190
+ return formatMoney(node.amount);
191
+ }
192
+
193
+ function rowButtonClass(row: FlowRow): string {
194
+ if (row.isCurrent) {
195
+ return 'border-amber-300 bg-amber-50/70 shadow-card dark:border-amber-700/70 dark:bg-amber-950/20';
196
+ }
197
+ return 'border-ngb-border bg-ngb-card hover:border-ngb-accent/40 hover:bg-ngb-surface';
198
+ }
199
+
200
+ function rowPaddingLeft(level: number): string {
201
+ return `${level * TREE_INDENT_PX}px`;
202
+ }
203
+
204
+ function lineLeft(level: number): string {
205
+ return `${level * TREE_INDENT_PX + TREE_LINE_LEFT_PX}px`;
206
+ }
207
+
208
+ function ancestorLineStyle(level: number): Record<string, string> {
209
+ return {
210
+ left: lineLeft(level),
211
+ top: `-${TREE_ROW_GAP_PX}px`,
212
+ height: `calc(100% + ${TREE_ROW_GAP_PX * 2}px)`,
213
+ };
214
+ }
215
+
216
+ function parentStemStyle(level: number): Record<string, string> {
217
+ return {
218
+ left: lineLeft(level - 1),
219
+ top: `-${TREE_ROW_GAP_PX}px`,
220
+ height: `calc(50% + ${TREE_ROW_GAP_PX}px)`,
221
+ };
222
+ }
223
+
224
+ function parentElbowStyle(level: number): Record<string, string> {
225
+ return {
226
+ left: lineLeft(level - 1),
227
+ top: '50%',
228
+ width: `${TREE_INDENT_PX - TREE_LINE_LEFT_PX + 1}px`,
229
+ };
230
+ }
231
+
232
+ function openSourceDocument(): void {
233
+ if (!documentType.value || !documentId.value) return;
234
+ void router.push(documentRoute.value);
235
+ }
236
+
237
+ async function goBack(): Promise<void> {
238
+ const explicitBack = resolveBackTarget(route);
239
+ if (explicitBack && explicitBack !== documentRoute.value) {
240
+ await router.replace(documentRoute.value);
241
+ return;
242
+ }
243
+
244
+ await navigateBack(router, route, documentRoute.value);
245
+ }
246
+
247
+ function openNode(node: RelationshipGraphNode): void {
248
+ if (!node?.typeCode || !node?.entityId) return;
249
+ void router.push(withBackTarget(buildDocumentFullPageUrl(node.typeCode, node.entityId), currentRouteBackTarget(route)));
250
+ }
251
+
252
+ async function copyShare(): Promise<void> {
253
+ if (!canShareLink.value) return;
254
+ await copyAppLink(router, toasts, shareRoute.value, {
255
+ title: 'Document flow link copied',
256
+ message: 'Shareable document flow page link copied to clipboard.',
257
+ });
258
+ }
259
+
260
+ async function refreshPage(): Promise<void> {
261
+ await load();
262
+ }
263
+
264
+ async function load(): Promise<void> {
265
+ if (!documentType.value || !documentId.value) {
266
+ error.value = 'Document type or id is missing.';
267
+ return;
268
+ }
269
+
270
+ loading.value = true;
271
+ error.value = null;
272
+ try {
273
+ graph.value = await getConfiguredNgbEditor().loadDocumentGraph(
274
+ documentType.value,
275
+ documentId.value,
276
+ DEFAULT_DEPTH,
277
+ DEFAULT_MAX_NODES,
278
+ );
279
+ } catch (cause) {
280
+ error.value = toErrorMessage(cause, 'Could not load document flow.');
281
+ graph.value = null;
282
+ } finally {
283
+ loading.value = false;
284
+ }
285
+ }
286
+
287
+ watch(
288
+ () => [documentType.value, documentId.value],
289
+ () => {
290
+ void load();
291
+ },
292
+ { immediate: true },
293
+ );
294
+ </script>
295
+
296
+ <template>
297
+ <div class="h-full min-h-0 flex flex-col bg-ngb-bg">
298
+ <NgbPageHeader :title="title" can-back @back="goBack">
299
+ <template #secondary>
300
+ <NgbBadge :tone="statusTone">{{ statusLabel }}</NgbBadge>
301
+ </template>
302
+
303
+ <template #actions>
304
+ <button
305
+ class="ngb-iconbtn"
306
+ :disabled="loading || !documentId"
307
+ title="Open document"
308
+ @click="openSourceDocument"
309
+ >
310
+ <NgbIcon name="edit" />
311
+ </button>
312
+
313
+ <button
314
+ v-if="canShareLink"
315
+ class="ngb-iconbtn"
316
+ :disabled="loading"
317
+ title="Share link"
318
+ @click="copyShare"
319
+ >
320
+ <NgbIcon name="share" />
321
+ </button>
322
+
323
+ <button
324
+ class="ngb-iconbtn"
325
+ :disabled="loading"
326
+ title="Refresh"
327
+ @click="refreshPage"
328
+ >
329
+ <NgbIcon name="refresh" />
330
+ </button>
331
+ </template>
332
+ </NgbPageHeader>
333
+
334
+ <div class="p-6 pt-4 flex-1 min-h-0 overflow-auto">
335
+ <div
336
+ v-if="loading"
337
+ class="rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-6 text-sm text-ngb-muted"
338
+ >
339
+ Loading document flow…
340
+ </div>
341
+
342
+ <div
343
+ v-else-if="error"
344
+ class="rounded-[var(--ngb-radius)] border border-red-200 bg-red-50 p-4 text-sm text-ngb-danger dark:border-red-900/50 dark:bg-red-950/30"
345
+ >
346
+ {{ error }}
347
+ </div>
348
+
349
+ <div
350
+ v-else-if="!flowRows.length"
351
+ class="rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-6 text-sm text-ngb-muted"
352
+ >
353
+ This document has no related documents yet.
354
+ </div>
355
+
356
+ <div v-else class="space-y-[18px] py-2">
357
+ <div
358
+ v-for="row in flowRows"
359
+ :key="row.key"
360
+ class="relative"
361
+ :style="{ paddingLeft: rowPaddingLeft(row.level) }"
362
+ >
363
+ <div class="pointer-events-none absolute inset-y-0 left-0" :style="{ width: rowPaddingLeft(row.level) }">
364
+ <span
365
+ v-for="(hasNext, index) in row.ancestorHasNext"
366
+ v-show="hasNext"
367
+ :key="`${row.key}:ancestor:${index}`"
368
+ class="absolute border-l-[3px] border-slate-300/95 dark:border-slate-700/95"
369
+ :style="ancestorLineStyle(index)"
370
+ />
371
+
372
+ <template v-if="row.level > 0">
373
+ <span
374
+ class="absolute border-l-[3px] border-slate-300/95 dark:border-slate-700/95"
375
+ :style="parentStemStyle(row.level)"
376
+ />
377
+ <span
378
+ class="absolute border-t-[3px] border-slate-300/95 dark:border-slate-700/95"
379
+ :style="parentElbowStyle(row.level)"
380
+ />
381
+ </template>
382
+ </div>
383
+
384
+ <button
385
+ type="button"
386
+ class="group w-full min-w-0 rounded-[var(--ngb-radius)] border text-left transition-colors"
387
+ :class="rowButtonClass(row)"
388
+ :title="displayForNode(row.node)"
389
+ @click="openNode(row.node)"
390
+ >
391
+ <div class="grid grid-cols-[30px,minmax(0,1fr),160px] items-center gap-3 px-4 py-3.5">
392
+ <div class="flex items-center justify-center">
393
+ <NgbStatusIcon :status="statusIconStatus(row.node)" />
394
+ </div>
395
+
396
+ <div class="min-w-0 truncate text-sm font-medium text-ngb-text">
397
+ {{ displayForNode(row.node) }}
398
+ </div>
399
+
400
+ <div class="text-right text-sm font-semibold tabular-nums text-ngb-text">
401
+ {{ amountLabel(row.node) }}
402
+ </div>
403
+ </div>
404
+ </button>
405
+ </div>
406
+ </div>
407
+ </div>
408
+ </div>
409
+ </template>