@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,712 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
3
+ import { useRoute, useRouter } from 'vue-router';
4
+
5
+ import NgbIcon from '../primitives/NgbIcon.vue';
6
+ import { useMetadataStore } from '../metadata/store';
7
+ import { dataTypeKind } from '../metadata/dataTypes';
8
+ import { formatLooseEntityValue, formatTypedEntityValue } from '../metadata/entityValueFormatting';
9
+ import { isReferenceValue } from '../metadata/entityModel';
10
+ import { lookupHintFromSource } from '../metadata/lookup';
11
+ import type {
12
+ ColumnMetadata,
13
+ DocumentTypeMetadata,
14
+ EntityFormModel,
15
+ FieldMetadata,
16
+ LookupHint,
17
+ } from '../metadata/types';
18
+ import { isNonEmptyGuid } from '../utils/guid';
19
+ import { toErrorMessage } from '../utils/errorMessage';
20
+ import { navigateBack, resolveBackTarget } from '../router/backNavigation';
21
+
22
+ import { getConfiguredNgbEditor, resolveNgbEditorPrintBehavior } from './config';
23
+ import { resolveDocumentReopenTarget } from './documentNavigation';
24
+ import { documentStatusLabel, normalizeDocumentStatusValue } from './documentStatus';
25
+ import type { DocumentRecord } from './types';
26
+
27
+ type PrintableFieldCell = {
28
+ key: string;
29
+ label: string;
30
+ value: string;
31
+ };
32
+
33
+ type PrintableFieldRow = {
34
+ key: string;
35
+ cells: PrintableFieldCell[];
36
+ };
37
+
38
+ type PrintableSection = {
39
+ key: string;
40
+ title: string;
41
+ rows: PrintableFieldRow[];
42
+ };
43
+
44
+ type PrintablePartSection = {
45
+ key: string;
46
+ title: string;
47
+ columns: Array<{ key: string; label: string; align: 'left' | 'right' }>;
48
+ rows: Array<{ key: string; cells: Record<string, string> }>;
49
+ };
50
+
51
+ const route = useRoute();
52
+ const router = useRouter();
53
+ const metadataStore = useMetadataStore();
54
+ const editorConfig = getConfiguredNgbEditor();
55
+ const lookupStore = editorConfig.lookupStore ?? null;
56
+
57
+ const loading = ref(false);
58
+ const error = ref<string | null>(null);
59
+ const metadata = ref<DocumentTypeMetadata | null>(null);
60
+ const documentRecord = ref<DocumentRecord | null>(null);
61
+ const autoPrintTriggered = ref(false);
62
+ const printedAt = ref(new Date());
63
+
64
+ const documentType = computed(() => String(route.params.documentType ?? '').trim());
65
+ const documentId = computed(() => String(route.params.id ?? '').trim());
66
+ const autoPrintRequested = computed(() => {
67
+ const raw = Array.isArray(route.query.autoprint) ? route.query.autoprint[0] : route.query.autoprint;
68
+ return String(raw ?? '').trim() === '1';
69
+ });
70
+
71
+ const documentRoute = computed(() => {
72
+ if (!documentType.value || !documentId.value) return '/';
73
+ return resolveDocumentReopenTarget(route, documentType.value, documentId.value);
74
+ });
75
+
76
+ async function goBack(): Promise<void> {
77
+ const explicitBack = resolveBackTarget(route);
78
+ if (explicitBack && explicitBack !== documentRoute.value) {
79
+ await router.replace(documentRoute.value);
80
+ return;
81
+ }
82
+
83
+ await navigateBack(router, route, documentRoute.value);
84
+ }
85
+
86
+ const sheetTitle = computed(() => {
87
+ const display = String(documentRecord.value?.display ?? '').trim();
88
+ if (display) return display;
89
+
90
+ const number = String(documentRecord.value?.number ?? '').trim();
91
+ const typeLabel = String(metadata.value?.displayName ?? 'Document').trim();
92
+ if (number) return `${typeLabel} ${number}`;
93
+ return typeLabel || 'Document';
94
+ });
95
+
96
+ const normalizedStatus = computed(() => normalizeDocumentStatusValue(documentRecord.value?.status));
97
+ const statusLabel = computed(() => documentStatusLabel(normalizedStatus.value));
98
+
99
+ const statusClass = computed(() => {
100
+ const status = normalizedStatus.value;
101
+ if (status === 2) return 'print-status posted';
102
+ if (status === 3) return 'print-status deleted';
103
+ return 'print-status draft';
104
+ });
105
+
106
+ const printableSections = computed<PrintableSection[]>(() => {
107
+ const form = metadata.value?.form;
108
+ const fields = documentRecord.value?.payload?.fields ?? {};
109
+ if (!form?.sections) return [];
110
+
111
+ return form.sections
112
+ .map((section, sectionIndex) => ({
113
+ key: `section:${section.title || sectionIndex}`,
114
+ title: String(section.title ?? '').trim() || 'Main',
115
+ rows: (section.rows ?? [])
116
+ .map((row, rowIndex) => ({
117
+ key: `row:${sectionIndex}:${rowIndex}`,
118
+ cells: (row.fields ?? [])
119
+ .filter((field) => !isHiddenDocumentField(field))
120
+ .map((field) => ({
121
+ key: field.key,
122
+ label: field.label,
123
+ value: formatFieldValue(field, (fields as EntityFormModel)[field.key]),
124
+ })),
125
+ }))
126
+ .filter((row) => row.cells.length > 0),
127
+ }))
128
+ .filter((section) => section.rows.length > 0);
129
+ });
130
+
131
+ const printableParts = computed<PrintablePartSection[]>(() => {
132
+ const partsMeta = metadata.value?.parts ?? [];
133
+ const partsPayload = documentRecord.value?.payload?.parts ?? {};
134
+
135
+ return partsMeta
136
+ .map((part) => {
137
+ const rows = Array.isArray(partsPayload?.[part.partCode]?.rows) ? partsPayload[part.partCode]!.rows : [];
138
+ const columns = (part.list?.columns ?? []).map((column) => ({
139
+ key: column.key,
140
+ label: column.label,
141
+ align:
142
+ dataTypeKind(column.dataType) === 'Money'
143
+ || dataTypeKind(column.dataType) === 'Decimal'
144
+ || dataTypeKind(column.dataType) === 'Int32'
145
+ ? 'right'
146
+ : 'left',
147
+ }));
148
+
149
+ return {
150
+ key: part.partCode,
151
+ title: String(part.title ?? part.partCode).trim() || part.partCode,
152
+ columns,
153
+ rows: rows.map((row, index) => ({
154
+ key: `${part.partCode}:${index}`,
155
+ cells: Object.fromEntries(
156
+ columns.map((column) => {
157
+ const metaColumn = (part.list?.columns ?? []).find((entry) => entry.key === column.key);
158
+ return [column.key, formatPartCell(metaColumn ?? null, row?.[column.key])];
159
+ }),
160
+ ),
161
+ })),
162
+ };
163
+ })
164
+ .filter((part) => part.columns.length > 0 && part.rows.length > 0);
165
+ });
166
+
167
+ watch(
168
+ () => sheetTitle.value,
169
+ (next) => {
170
+ if (typeof document === 'undefined') return;
171
+ document.title = String(next ?? '').trim() || 'Document print';
172
+ },
173
+ { immediate: true },
174
+ );
175
+
176
+ function handleBeforePrint() {
177
+ if (typeof document === 'undefined') return;
178
+ document.title = ' ';
179
+ }
180
+
181
+ function handleAfterPrint() {
182
+ if (typeof document === 'undefined') return;
183
+ document.title = String(sheetTitle.value ?? '').trim() || 'Document print';
184
+ }
185
+
186
+ onMounted(() => {
187
+ if (typeof window === 'undefined') return;
188
+ window.addEventListener('beforeprint', handleBeforePrint);
189
+ window.addEventListener('afterprint', handleAfterPrint);
190
+ });
191
+
192
+ onBeforeUnmount(() => {
193
+ if (typeof window === 'undefined') return;
194
+ window.removeEventListener('beforeprint', handleBeforePrint);
195
+ window.removeEventListener('afterprint', handleAfterPrint);
196
+ handleAfterPrint();
197
+ });
198
+
199
+ watch(
200
+ () => [documentType.value, documentId.value] as const,
201
+ () => {
202
+ autoPrintTriggered.value = false;
203
+ printedAt.value = new Date();
204
+ void load();
205
+ },
206
+ { immediate: true },
207
+ );
208
+
209
+ watch(
210
+ () => [loading.value, error.value, autoPrintRequested.value, metadata.value?.documentType, documentRecord.value?.id] as const,
211
+ async () => {
212
+ if (!autoPrintRequested.value || autoPrintTriggered.value || loading.value || error.value || !metadata.value || !documentRecord.value) return;
213
+ autoPrintTriggered.value = true;
214
+ await nextTick();
215
+ window.setTimeout(() => {
216
+ if (typeof window !== 'undefined') window.print();
217
+ }, 120);
218
+ },
219
+ );
220
+
221
+ async function load() {
222
+ if (!documentType.value || !documentId.value) return;
223
+
224
+ loading.value = true;
225
+ error.value = null;
226
+
227
+ try {
228
+ const [nextMetadata, nextDocument] = await Promise.all([
229
+ metadataStore.ensureDocumentType(documentType.value),
230
+ editorConfig.loadDocumentById(documentType.value, documentId.value),
231
+ ]);
232
+
233
+ metadata.value = nextMetadata;
234
+ documentRecord.value = nextDocument;
235
+ await prefetchLookupLabels(nextMetadata, nextDocument);
236
+ } catch (cause) {
237
+ error.value = toErrorMessage(cause, 'Failed to load the print preview.');
238
+ } finally {
239
+ loading.value = false;
240
+ }
241
+ }
242
+
243
+ function triggerPrint() {
244
+ if (typeof window === 'undefined') return;
245
+ window.print();
246
+ }
247
+
248
+ function isHiddenDocumentField(field: FieldMetadata): boolean {
249
+ return field.key === 'display' || field.key === 'number';
250
+ }
251
+
252
+ function resolveLookupHint(fieldKey: string, lookup?: FieldMetadata['lookup'] | ColumnMetadata['lookup'] | null): LookupHint | null {
253
+ return resolveNgbEditorPrintBehavior().resolveLookupHint?.({
254
+ documentType: documentType.value,
255
+ fieldKey,
256
+ lookup: lookup ?? null,
257
+ }) ?? lookupHintFromSource(lookup ?? null);
258
+ }
259
+
260
+ function labelForHint(hint: LookupHint, id: string): string {
261
+ if (!lookupStore) return id;
262
+ if (hint.kind === 'catalog') return lookupStore.labelForCatalog(hint.catalogType, id);
263
+ if (hint.kind === 'coa') return lookupStore.labelForCoa(id);
264
+ return lookupStore.labelForAnyDocument(hint.documentTypes, id);
265
+ }
266
+
267
+ function formatValueWithHint(value: unknown, hint: LookupHint | null): string | null {
268
+ if (isReferenceValue(value)) return formatLooseEntityValue(value);
269
+ if (!hint || !isNonEmptyGuid(value)) return null;
270
+ return labelForHint(hint, value);
271
+ }
272
+
273
+ function formatFieldValue(field: FieldMetadata, value: unknown): string {
274
+ const hint = resolveLookupHint(field.key, field.lookup);
275
+ const fromHint = formatValueWithHint(value, hint);
276
+ if (fromHint) return fromHint;
277
+ return formatTypedEntityValue(field.dataType, value);
278
+ }
279
+
280
+ function formatPartCell(column: ColumnMetadata | null, value: unknown): string {
281
+ if (!column) return formatLooseEntityValue(value);
282
+ const hint = resolveLookupHint(column.key, column.lookup);
283
+ const fromHint = formatValueWithHint(value, hint);
284
+ if (fromHint) return fromHint;
285
+ return formatTypedEntityValue(column.dataType, value);
286
+ }
287
+
288
+ type LookupBuckets = {
289
+ catalogs: Map<string, Set<string>>;
290
+ documents: Map<string, { types: string[]; ids: Set<string> }>;
291
+ coaIds: Set<string>;
292
+ };
293
+
294
+ function collectLookupValue(hint: LookupHint | null, value: unknown, buckets: LookupBuckets) {
295
+ if (!hint) return;
296
+ if (isReferenceValue(value) && value.display.trim().length > 0) return;
297
+
298
+ const id = isReferenceValue(value) ? value.id : (isNonEmptyGuid(value) ? value : null);
299
+ if (!id) return;
300
+
301
+ if (hint.kind === 'catalog') {
302
+ const ids = buckets.catalogs.get(hint.catalogType) ?? new Set<string>();
303
+ ids.add(id);
304
+ buckets.catalogs.set(hint.catalogType, ids);
305
+ return;
306
+ }
307
+
308
+ if (hint.kind === 'coa') {
309
+ buckets.coaIds.add(id);
310
+ return;
311
+ }
312
+
313
+ const key = hint.documentTypes.join('|');
314
+ const entry = buckets.documents.get(key) ?? { types: [...hint.documentTypes], ids: new Set<string>() };
315
+ entry.ids.add(id);
316
+ buckets.documents.set(key, entry);
317
+ }
318
+
319
+ async function prefetchLookupLabels(meta: DocumentTypeMetadata, doc: DocumentRecord) {
320
+ if (!lookupStore) return;
321
+
322
+ const buckets: LookupBuckets = {
323
+ catalogs: new Map(),
324
+ documents: new Map(),
325
+ coaIds: new Set(),
326
+ };
327
+
328
+ const fields = doc.payload?.fields ?? {};
329
+ for (const section of meta.form?.sections ?? []) {
330
+ for (const row of section.rows ?? []) {
331
+ for (const field of row.fields ?? []) {
332
+ collectLookupValue(resolveLookupHint(field.key, field.lookup), (fields as EntityFormModel)[field.key], buckets);
333
+ }
334
+ }
335
+ }
336
+
337
+ const partsPayload = doc.payload?.parts ?? {};
338
+ for (const part of meta.parts ?? []) {
339
+ const rows = Array.isArray(partsPayload?.[part.partCode]?.rows) ? partsPayload[part.partCode]!.rows : [];
340
+ for (const row of rows) {
341
+ for (const column of part.list?.columns ?? []) {
342
+ collectLookupValue(resolveLookupHint(column.key, column.lookup), row?.[column.key], buckets);
343
+ }
344
+ }
345
+ }
346
+
347
+ const tasks: Promise<unknown>[] = [];
348
+
349
+ for (const [catalogType, ids] of buckets.catalogs.entries()) {
350
+ tasks.push(Promise.resolve().then(() => lookupStore.ensureCatalogLabels(catalogType, Array.from(ids))));
351
+ }
352
+
353
+ if (buckets.coaIds.size > 0) {
354
+ tasks.push(Promise.resolve().then(() => lookupStore.ensureCoaLabels(Array.from(buckets.coaIds))));
355
+ }
356
+
357
+ for (const entry of buckets.documents.values()) {
358
+ tasks.push(Promise.resolve().then(() => lookupStore.ensureAnyDocumentLabels(entry.types, Array.from(entry.ids))));
359
+ }
360
+
361
+ if (tasks.length > 0) {
362
+ await Promise.allSettled(tasks);
363
+ }
364
+ }
365
+ </script>
366
+
367
+ <template>
368
+ <div class="document-print-root">
369
+ <div class="document-print-toolbar">
370
+ <button class="document-print-toolbar-button" @click="goBack">
371
+ <NgbIcon name="arrow-left" :size="16" />
372
+ <span>Back</span>
373
+ </button>
374
+
375
+ <div class="document-print-toolbar-title">Print preview</div>
376
+
377
+ <button class="document-print-toolbar-button" :disabled="loading || !!error" @click="triggerPrint">
378
+ <NgbIcon name="printer" :size="16" />
379
+ <span>Print</span>
380
+ </button>
381
+ </div>
382
+
383
+ <main class="document-print-shell">
384
+ <div v-if="error" class="document-print-error">
385
+ {{ error }}
386
+ </div>
387
+
388
+ <div v-else-if="loading" class="document-print-loading">
389
+ Loading print preview...
390
+ </div>
391
+
392
+ <article v-else class="document-print-sheet">
393
+ <header class="document-print-header">
394
+ <div class="document-print-header-main">
395
+ <h1 class="document-print-title">{{ sheetTitle }}</h1>
396
+ </div>
397
+
398
+ <div class="document-print-meta">
399
+ <div>
400
+ <div class="document-print-meta-label">Status</div>
401
+ <div :class="statusClass">{{ statusLabel }}</div>
402
+ </div>
403
+
404
+ <div>
405
+ <div class="document-print-meta-label">Printed</div>
406
+ <div class="document-print-meta-value">{{ printedAt.toLocaleString() }}</div>
407
+ </div>
408
+ </div>
409
+ </header>
410
+
411
+ <section v-for="section in printableSections" :key="section.key" class="document-print-section">
412
+ <h2 class="document-print-section-title">{{ section.title }}</h2>
413
+
414
+ <div class="document-print-field-rows">
415
+ <div v-for="row in section.rows" :key="row.key" class="document-print-field-row">
416
+ <div v-for="cell in row.cells" :key="cell.key" class="document-print-field-cell">
417
+ <div class="document-print-field-label">{{ cell.label }}</div>
418
+ <div class="document-print-field-value">{{ cell.value }}</div>
419
+ </div>
420
+ </div>
421
+ </div>
422
+ </section>
423
+
424
+ <section v-for="part in printableParts" :key="part.key" class="document-print-section">
425
+ <h2 class="document-print-section-title">{{ part.title }}</h2>
426
+
427
+ <div class="document-print-table-wrap">
428
+ <table class="document-print-table">
429
+ <thead>
430
+ <tr>
431
+ <th
432
+ v-for="column in part.columns"
433
+ :key="column.key"
434
+ :class="column.align === 'right' ? 'align-right' : ''"
435
+ >
436
+ {{ column.label }}
437
+ </th>
438
+ </tr>
439
+ </thead>
440
+ <tbody>
441
+ <tr v-for="row in part.rows" :key="row.key">
442
+ <td
443
+ v-for="column in part.columns"
444
+ :key="column.key"
445
+ :class="column.align === 'right' ? 'align-right' : ''"
446
+ >
447
+ {{ row.cells[column.key] }}
448
+ </td>
449
+ </tr>
450
+ </tbody>
451
+ </table>
452
+ </div>
453
+ </section>
454
+ </article>
455
+ </main>
456
+ </div>
457
+ </template>
458
+
459
+ <style scoped>
460
+ .document-print-root {
461
+ min-height: 100vh;
462
+ background: var(--ngb-bg);
463
+ color: var(--ngb-text);
464
+ }
465
+
466
+ .document-print-toolbar {
467
+ display: flex;
468
+ align-items: center;
469
+ justify-content: space-between;
470
+ gap: 12px;
471
+ padding: 16px 24px;
472
+ border-bottom: 1px solid var(--ngb-border);
473
+ background: var(--ngb-card);
474
+ }
475
+
476
+ .document-print-toolbar-title {
477
+ font-size: 14px;
478
+ font-weight: 600;
479
+ color: var(--ngb-muted);
480
+ }
481
+
482
+ .document-print-toolbar-button {
483
+ display: inline-flex;
484
+ align-items: center;
485
+ gap: 8px;
486
+ height: 36px;
487
+ padding: 0 12px;
488
+ border: 1px solid var(--ngb-border);
489
+ border-radius: var(--ngb-radius);
490
+ background: var(--ngb-card);
491
+ color: var(--ngb-text);
492
+ cursor: pointer;
493
+ transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
494
+ }
495
+
496
+ .document-print-toolbar-button:hover:not(:disabled) {
497
+ background: var(--ngb-bg);
498
+ }
499
+
500
+ .document-print-toolbar-button:disabled {
501
+ opacity: 0.5;
502
+ cursor: not-allowed;
503
+ }
504
+
505
+ .document-print-shell {
506
+ padding: 24px;
507
+ }
508
+
509
+ .document-print-loading,
510
+ .document-print-error {
511
+ max-width: 980px;
512
+ margin: 0 auto;
513
+ padding: 18px 20px;
514
+ border-radius: var(--ngb-radius);
515
+ border: 1px solid var(--ngb-border);
516
+ background: var(--ngb-card);
517
+ }
518
+
519
+ .document-print-error {
520
+ color: var(--ngb-danger);
521
+ border-color: color-mix(in srgb, var(--ngb-danger) 30%, var(--ngb-border));
522
+ }
523
+
524
+ .document-print-sheet {
525
+ width: min(980px, 100%);
526
+ margin: 0 auto;
527
+ background: #fff;
528
+ color: #111827;
529
+ border: 1px solid #dbe3ef;
530
+ border-radius: var(--ngb-radius);
531
+ box-shadow: 0 18px 46px rgba(15, 23, 42, 0.08);
532
+ padding: 28px 32px 36px;
533
+ }
534
+
535
+ .document-print-header {
536
+ display: flex;
537
+ flex-direction: column;
538
+ align-items: stretch;
539
+ gap: 24px;
540
+ padding-bottom: 20px;
541
+ border-bottom: 1px solid #dbe3ef;
542
+ }
543
+
544
+ .document-print-header-main {
545
+ min-width: 0;
546
+ flex: 1 1 auto;
547
+ }
548
+
549
+ .document-print-title {
550
+ margin: 0;
551
+ font-size: 28px;
552
+ line-height: 1.2;
553
+ }
554
+
555
+ .document-print-meta {
556
+ flex: 0 0 auto;
557
+ align-self: flex-end;
558
+ display: grid;
559
+ grid-template-columns: repeat(2, minmax(140px, max-content));
560
+ gap: 12px 24px;
561
+ }
562
+
563
+ .document-print-meta-label {
564
+ font-size: 11px;
565
+ font-weight: 700;
566
+ letter-spacing: 0.06em;
567
+ text-transform: uppercase;
568
+ color: #6b7280;
569
+ }
570
+
571
+ .document-print-meta-value {
572
+ margin-top: 4px;
573
+ font-size: 14px;
574
+ line-height: 1.4;
575
+ }
576
+
577
+ .print-status {
578
+ display: inline-flex;
579
+ align-items: center;
580
+ min-height: 28px;
581
+ padding: 0 10px;
582
+ border-radius: 999px;
583
+ font-size: 13px;
584
+ font-weight: 600;
585
+ margin-top: 4px;
586
+ }
587
+
588
+ .print-status.draft {
589
+ color: #475569;
590
+ background: #f1f5f9;
591
+ }
592
+
593
+ .print-status.posted {
594
+ color: #166534;
595
+ background: #dcfce7;
596
+ }
597
+
598
+ .print-status.deleted {
599
+ color: #991b1b;
600
+ background: #fee2e2;
601
+ }
602
+
603
+ .document-print-section {
604
+ margin-top: 24px;
605
+ break-inside: avoid;
606
+ }
607
+
608
+ .document-print-section-title {
609
+ margin: 0 0 12px;
610
+ font-size: 16px;
611
+ line-height: 1.3;
612
+ }
613
+
614
+ .document-print-field-rows {
615
+ display: grid;
616
+ gap: 12px;
617
+ }
618
+
619
+ .document-print-field-row {
620
+ display: grid;
621
+ grid-template-columns: repeat(2, minmax(0, 1fr));
622
+ gap: 12px 20px;
623
+ }
624
+
625
+ .document-print-field-cell {
626
+ min-width: 0;
627
+ padding: 12px 14px;
628
+ border: 1px solid #e5e7eb;
629
+ border-radius: var(--ngb-radius);
630
+ }
631
+
632
+ .document-print-field-cell:only-child {
633
+ grid-column: 1 / -1;
634
+ }
635
+
636
+ .document-print-field-label {
637
+ font-size: 11px;
638
+ font-weight: 700;
639
+ letter-spacing: 0.05em;
640
+ text-transform: uppercase;
641
+ color: #6b7280;
642
+ }
643
+
644
+ .document-print-field-value {
645
+ margin-top: 6px;
646
+ font-size: 14px;
647
+ line-height: 1.45;
648
+ white-space: pre-wrap;
649
+ word-break: break-word;
650
+ }
651
+
652
+ .document-print-table-wrap {
653
+ overflow: hidden;
654
+ border: 1px solid #dbe3ef;
655
+ border-radius: var(--ngb-radius);
656
+ }
657
+
658
+ .document-print-table {
659
+ width: 100%;
660
+ border-collapse: collapse;
661
+ }
662
+
663
+ .document-print-table th,
664
+ .document-print-table td {
665
+ padding: 10px 12px;
666
+ border-bottom: 1px solid #e5e7eb;
667
+ text-align: left;
668
+ font-size: 13px;
669
+ line-height: 1.4;
670
+ vertical-align: top;
671
+ }
672
+
673
+ .document-print-table th {
674
+ background: #f8fafc;
675
+ font-size: 11px;
676
+ font-weight: 700;
677
+ letter-spacing: 0.05em;
678
+ text-transform: uppercase;
679
+ color: #6b7280;
680
+ }
681
+
682
+ .document-print-table tr:last-child td {
683
+ border-bottom: 0;
684
+ }
685
+
686
+ .document-print-table .align-right {
687
+ text-align: right;
688
+ }
689
+
690
+ @media print {
691
+ .document-print-root {
692
+ background: #fff;
693
+ }
694
+
695
+ .document-print-toolbar {
696
+ display: none;
697
+ }
698
+
699
+ .document-print-shell {
700
+ padding: 0;
701
+ }
702
+
703
+ .document-print-sheet {
704
+ width: 100%;
705
+ margin: 0;
706
+ border: 0;
707
+ box-shadow: none;
708
+ border-radius: 0;
709
+ padding: 0;
710
+ }
711
+ }
712
+ </style>