@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,796 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+
5
+ import { useAuthStore } from '../auth'
6
+ import { ApiError } from '../api/http'
7
+ import NgbDrawer from '../components/NgbDrawer.vue'
8
+ import NgbFormLayout from '../components/forms/NgbFormLayout.vue'
9
+ import NgbFormRow from '../components/forms/NgbFormRow.vue'
10
+ import NgbFormSection from '../components/forms/NgbFormSection.vue'
11
+ import NgbValidationSummary from '../components/forms/NgbValidationSummary.vue'
12
+ import { documentStatusLabel, documentStatusTone, normalizeDocumentStatusValue } from '../editor/documentStatus'
13
+ import NgbEntityAuditSidebar from '../editor/NgbEntityAuditSidebar.vue'
14
+ import NgbBadge from '../primitives/NgbBadge.vue'
15
+ import NgbCheckbox from '../primitives/NgbCheckbox.vue'
16
+ import NgbDatePicker from '../primitives/NgbDatePicker.vue'
17
+ import NgbIcon from '../primitives/NgbIcon.vue'
18
+ import NgbInput from '../primitives/NgbInput.vue'
19
+ import NgbTabs from '../primitives/NgbTabs.vue'
20
+ import NgbSelect from '../primitives/NgbSelect.vue'
21
+ import { useToasts } from '../primitives/toast'
22
+ import NgbPageHeader from '../site/NgbPageHeader.vue'
23
+ import { navigateBack } from '../router/backNavigation'
24
+ import { copyAppLink } from '../router/shareLink'
25
+ import {
26
+ approveGeneralJournalEntry,
27
+ createGeneralJournalEntryDraft,
28
+ getGeneralJournalEntry,
29
+ markGeneralJournalEntryForDeletion,
30
+ postGeneralJournalEntry,
31
+ rejectGeneralJournalEntry,
32
+ replaceGeneralJournalEntryLines,
33
+ reverseGeneralJournalEntry,
34
+ submitGeneralJournalEntry,
35
+ unmarkGeneralJournalEntryForDeletion,
36
+ updateGeneralJournalEntryHeader,
37
+ } from './generalJournalEntryApi'
38
+ import {
39
+ createGeneralJournalEntryLine,
40
+ formatGeneralJournalEntryMoney,
41
+ generalJournalEntryApprovalStateLabel,
42
+ generalJournalEntrySourceLabel,
43
+ normalizeDateOnly,
44
+ normalizeGeneralJournalEntryApprovalState,
45
+ normalizeGeneralJournalEntrySource,
46
+ parseGeneralJournalEntryAmount,
47
+ todayDateOnly,
48
+ toUtcMidday,
49
+ } from './generalJournalEntry'
50
+ import NgbGeneralJournalEntryLinesEditor from './NgbGeneralJournalEntryLinesEditor.vue'
51
+ import { buildGeneralJournalEntriesPath, buildGeneralJournalEntriesListPath } from './navigation'
52
+ import type {
53
+ GeneralJournalEntryAccountContextDto,
54
+ GeneralJournalEntryDetailsDto,
55
+ GeneralJournalEntryEditorLineModel,
56
+ GeneralJournalEntryLineDto,
57
+ } from './generalJournalEntryTypes'
58
+
59
+ const props = withDefaults(defineProps<{
60
+ listPath?: string | null
61
+ }>(), {
62
+ listPath: null,
63
+ })
64
+
65
+ const route = useRoute()
66
+ const router = useRouter()
67
+ const toasts = useToasts()
68
+ const auth = useAuthStore()
69
+
70
+ const loading = ref(false)
71
+ const saving = ref(false)
72
+ const details = ref<GeneralJournalEntryDetailsDto | null>(null)
73
+ const errorMessages = ref<string[]>([])
74
+ const activeTab = ref('lines')
75
+ const auditOpen = ref(false)
76
+ const suppressRouteLoad = ref(false)
77
+
78
+ const draftDate = ref<string | null>(todayDateOnly())
79
+ const journalType = ref(1)
80
+ const reasonCode = ref('')
81
+ const memo = ref('')
82
+ const externalReference = ref('')
83
+ const autoReverse = ref(false)
84
+ const autoReverseOnUtc = ref<string | null>(null)
85
+ const lines = ref<GeneralJournalEntryEditorLineModel[]>([createGeneralJournalEntryLine('new-1')])
86
+
87
+ const rejectReason = ref('')
88
+ const reversalDate = ref<string | null>(todayDateOnly())
89
+ const reversePostImmediately = ref(true)
90
+
91
+ const journalTypeOptions = [
92
+ { value: 1, label: 'Standard' },
93
+ { value: 2, label: 'Reversing' },
94
+ { value: 3, label: 'Adjusting' },
95
+ { value: 4, label: 'Opening' },
96
+ { value: 5, label: 'Closing' },
97
+ ]
98
+
99
+ const tabs = [
100
+ { key: 'lines', label: 'Lines' },
101
+ { key: 'workflow', label: 'Workflow' },
102
+ ]
103
+
104
+ const currentId = computed(() => {
105
+ const raw = route.params.id
106
+ return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : null
107
+ })
108
+
109
+ const resolvedListPath = computed(() => {
110
+ const explicit = String(props.listPath ?? '').trim()
111
+ if (explicit) return explicit
112
+
113
+ const currentPath = String(route.path ?? '').trim()
114
+ if (!currentPath) return buildGeneralJournalEntriesListPath()
115
+ if (currentPath.endsWith('/new')) return currentPath.slice(0, -4)
116
+
117
+ const lastSlash = currentPath.lastIndexOf('/')
118
+ if (lastSlash <= 0) return buildGeneralJournalEntriesListPath()
119
+ return currentPath.slice(0, lastSlash)
120
+ })
121
+
122
+ const isNew = computed(() => !currentId.value)
123
+ const documentStatus = computed(() => normalizeDocumentStatusValue(details.value?.document?.status))
124
+ const approvalState = computed(() => normalizeGeneralJournalEntryApprovalState(details.value?.header?.approvalState))
125
+ const source = computed(() => normalizeGeneralJournalEntrySource(details.value?.header?.source))
126
+ const isManualSource = computed(() => source.value !== 2)
127
+ const isMarkedForDeletion = computed(() => !!details.value?.document?.isMarkedForDeletion || documentStatus.value === 3)
128
+
129
+ const canEditDraft = computed(() => !saving.value && (isNew.value || (isManualSource.value && documentStatus.value === 1 && approvalState.value === 1)))
130
+ const canSave = computed(() => canEditDraft.value)
131
+ const canSubmit = computed(() => !isNew.value && isManualSource.value && documentStatus.value === 1 && approvalState.value === 1 && !isMarkedForDeletion.value && !saving.value)
132
+ const canApprove = computed(() => !isNew.value && documentStatus.value === 1 && approvalState.value === 2 && !saving.value)
133
+ const canReject = computed(() => canApprove.value)
134
+ const canPost = computed(() => !isNew.value && documentStatus.value === 1 && approvalState.value === 3 && !saving.value)
135
+ const canReverse = computed(() => !isNew.value && documentStatus.value === 2 && !saving.value)
136
+ const canMarkForDeletion = computed(() => !isNew.value && isManualSource.value && documentStatus.value === 1 && !isMarkedForDeletion.value && !saving.value)
137
+ const canUnmarkForDeletion = computed(() => !isNew.value && isManualSource.value && isMarkedForDeletion.value && !saving.value)
138
+
139
+ const showSubmit = computed(() => canSubmit.value)
140
+ const showApprove = computed(() => !isNew.value && documentStatus.value === 1 && approvalState.value === 2)
141
+ const showReject = computed(() => showApprove.value)
142
+ const showPost = computed(() => !isNew.value && documentStatus.value === 1 && approvalState.value === 3)
143
+ const showReverse = computed(() => !isNew.value && documentStatus.value === 2)
144
+ const canShareLink = computed(() => !isNew.value && !!currentId.value)
145
+ const canOpenAudit = computed(() => !isNew.value && !!currentId.value)
146
+ const currentActorDisplay = computed(() => {
147
+ const value = String(auth.userName ?? '').trim()
148
+ return value || 'Current user'
149
+ })
150
+ const initiatedByDisplay = computed(() => resolveActorFieldDisplay(details.value?.header?.initiatedBy))
151
+ const submittedByDisplay = computed(() => resolveActorFieldDisplay(details.value?.header?.submittedBy))
152
+ const approvedByDisplay = computed(() => resolveActorFieldDisplay(details.value?.header?.approvedBy))
153
+ const rejectedByDisplay = computed(() => resolveActorFieldDisplay(details.value?.header?.rejectedBy))
154
+ const postedByDisplay = computed(() => resolveActorFieldDisplay(details.value?.header?.postedBy))
155
+ const reverseInitiatedByDisplay = computed(() =>
156
+ resolveActorFieldDisplay(source.value === 2 ? details.value?.header?.initiatedBy : null),
157
+ )
158
+
159
+ const pageTitle = computed(() => {
160
+ const display = String(details.value?.document?.display ?? '').trim()
161
+ if (display) return display
162
+ const number = String(details.value?.document?.number ?? '').trim()
163
+ if (number) return `General Journal Entry ${number}`
164
+ return isNew.value ? 'New General Journal Entry' : 'General Journal Entry'
165
+ })
166
+
167
+ const sourceDocumentLabel = computed(() => {
168
+ const value = String(details.value?.header?.reversalOfDocumentDisplay ?? '').trim()
169
+ return value || null
170
+ })
171
+ const preloadedAccountContexts = computed<Record<string, GeneralJournalEntryAccountContextDto | null>>(() =>
172
+ Object.fromEntries(
173
+ (details.value?.accountContexts ?? []).map((context) => [context.accountId, context]),
174
+ ),
175
+ )
176
+
177
+ const auditEntityKind = 1
178
+ const auditEntityId = computed(() => currentId.value)
179
+ const auditEntityTitle = computed(() => pageTitle.value)
180
+ const documentStatusBadgeTone = computed(() => documentStatusTone(documentStatus.value))
181
+
182
+ watch(
183
+ () => currentId.value,
184
+ () => {
185
+ if (suppressRouteLoad.value) return
186
+ void load()
187
+ },
188
+ { immediate: true },
189
+ )
190
+
191
+ function resetForNew() {
192
+ details.value = null
193
+ errorMessages.value = []
194
+ activeTab.value = 'lines'
195
+ draftDate.value = todayDateOnly()
196
+ journalType.value = 1
197
+ reasonCode.value = ''
198
+ memo.value = ''
199
+ externalReference.value = ''
200
+ autoReverse.value = false
201
+ autoReverseOnUtc.value = null
202
+ lines.value = [createGeneralJournalEntryLine('new-1')]
203
+ rejectReason.value = ''
204
+ reversalDate.value = todayDateOnly()
205
+ reversePostImmediately.value = true
206
+ }
207
+
208
+ async function applyDetails(dto: GeneralJournalEntryDetailsDto) {
209
+ details.value = dto
210
+ draftDate.value = dateOnlyFromIso(dto.dateUtc)
211
+ journalType.value = Number(dto.header.journalType ?? 1)
212
+ reasonCode.value = dto.header.reasonCode ?? ''
213
+ memo.value = dto.header.memo ?? ''
214
+ externalReference.value = dto.header.externalReference ?? ''
215
+ autoReverse.value = !!dto.header.autoReverse
216
+ autoReverseOnUtc.value = normalizeDateOnly(dto.header.autoReverseOnUtc)
217
+ rejectReason.value = dto.header.rejectReason ?? ''
218
+ reversalDate.value = todayDateOnly()
219
+ reversePostImmediately.value = true
220
+ lines.value = hydrateLines(dto.lines, dto.accountContexts ?? [])
221
+ }
222
+
223
+ async function load() {
224
+ errorMessages.value = []
225
+
226
+ if (!currentId.value) {
227
+ resetForNew()
228
+ return
229
+ }
230
+
231
+ loading.value = true
232
+ try {
233
+ const dto = await getGeneralJournalEntry(currentId.value)
234
+ await applyDetails(dto)
235
+ } catch (cause) {
236
+ errorMessages.value = extractErrorMessages(cause)
237
+ } finally {
238
+ loading.value = false
239
+ }
240
+ }
241
+
242
+ function dateOnlyFromIso(value: string | null | undefined): string | null {
243
+ return normalizeDateOnly(value)
244
+ }
245
+
246
+ function badgeToneForApproval(): 'neutral' | 'success' | 'warn' {
247
+ if (approvalState.value === 3) return 'success'
248
+ if (approvalState.value === 4) return 'warn'
249
+ return 'neutral'
250
+ }
251
+
252
+ function resolveActorFieldDisplay(value: string | null | undefined): string {
253
+ const raw = String(value ?? '').trim()
254
+ return raw || currentActorDisplay.value
255
+ }
256
+
257
+ function hydrateLines(
258
+ sourceLines: GeneralJournalEntryLineDto[],
259
+ accountContexts: GeneralJournalEntryAccountContextDto[],
260
+ ): GeneralJournalEntryEditorLineModel[] {
261
+ if (!sourceLines.length) return [createGeneralJournalEntryLine('existing-1')]
262
+ const contextsByAccount = Object.fromEntries(
263
+ accountContexts.map((context) => [context.accountId, context]),
264
+ ) as Record<string, GeneralJournalEntryAccountContextDto | null>
265
+
266
+ return sourceLines.map((line) => {
267
+ const context = contextsByAccount[line.accountId] ?? null
268
+ const dimensions: Record<string, { id: string; label: string }> = {}
269
+
270
+ for (const dim of line.dimensions ?? []) {
271
+ dimensions[dim.dimensionId] = {
272
+ id: dim.valueId,
273
+ label: String(dim.display ?? dim.valueId),
274
+ }
275
+ }
276
+
277
+ return {
278
+ clientKey: `line-${line.lineNo}`,
279
+ side: Number(line.side ?? 1),
280
+ account: {
281
+ id: line.accountId,
282
+ label: String(line.accountDisplay ?? (context ? `${context.code} — ${context.name}` : line.accountId)),
283
+ },
284
+ amount: String(line.amount ?? ''),
285
+ memo: line.memo ?? '',
286
+ dimensions,
287
+ }
288
+ })
289
+ }
290
+
291
+ function buildHeaderRequest() {
292
+ return {
293
+ updatedBy: currentActorDisplay.value,
294
+ journalType: Number(journalType.value),
295
+ reasonCode: reasonCode.value.trim() || null,
296
+ memo: memo.value.trim() || null,
297
+ externalReference: externalReference.value.trim() || null,
298
+ autoReverse: !!autoReverse.value,
299
+ autoReverseOnUtc: autoReverse.value ? normalizeDateOnly(autoReverseOnUtc.value) : null,
300
+ }
301
+ }
302
+
303
+ function buildLinesRequest() {
304
+ return {
305
+ updatedBy: currentActorDisplay.value,
306
+ lines: lines.value
307
+ .filter((line) => !!line.account?.id)
308
+ .map((line) => ({
309
+ side: Number(line.side || 1),
310
+ accountId: line.account!.id,
311
+ amount: parseGeneralJournalEntryAmount(line.amount),
312
+ memo: line.memo.trim() || null,
313
+ dimensions: Object.entries(line.dimensions ?? {})
314
+ .filter(([, item]) => !!item?.id)
315
+ .map(([dimensionId, item]) => ({ dimensionId, valueId: item!.id })),
316
+ })),
317
+ }
318
+ }
319
+
320
+ async function saveDraft(): Promise<boolean> {
321
+ if (!canSave.value) return false
322
+
323
+ saving.value = true
324
+ errorMessages.value = []
325
+ try {
326
+ let id = currentId.value
327
+ const headerRequest = buildHeaderRequest()
328
+ const linesRequest = buildLinesRequest()
329
+
330
+ if (!id) {
331
+ suppressRouteLoad.value = true
332
+ const created = await createGeneralJournalEntryDraft({
333
+ dateUtc: toUtcMidday(draftDate.value),
334
+ })
335
+ id = created.document.id
336
+ await router.replace(buildGeneralJournalEntriesPath(id, { basePath: resolvedListPath.value }))
337
+ }
338
+
339
+ await updateGeneralJournalEntryHeader(id!, headerRequest)
340
+ const finalDto = await replaceGeneralJournalEntryLines(id!, linesRequest)
341
+ await applyDetails(finalDto)
342
+
343
+ toasts.push({ title: 'Saved', message: 'Draft was saved.', tone: 'success' })
344
+ return true
345
+ } catch (cause) {
346
+ errorMessages.value = extractErrorMessages(cause)
347
+ return false
348
+ } finally {
349
+ suppressRouteLoad.value = false
350
+ saving.value = false
351
+ }
352
+ }
353
+
354
+ async function submit() {
355
+ const ok = await saveDraft()
356
+ if (!ok || !currentId.value) return
357
+
358
+ saving.value = true
359
+ errorMessages.value = []
360
+ try {
361
+ const dto = await submitGeneralJournalEntry(currentId.value, {})
362
+ await applyDetails(dto)
363
+ toasts.push({ title: 'Submitted', message: 'Journal entry was submitted.', tone: 'success' })
364
+ } catch (cause) {
365
+ errorMessages.value = extractErrorMessages(cause)
366
+ } finally {
367
+ saving.value = false
368
+ }
369
+ }
370
+
371
+ async function approve() {
372
+ if (!currentId.value) return
373
+ saving.value = true
374
+ errorMessages.value = []
375
+ try {
376
+ const dto = await approveGeneralJournalEntry(currentId.value, {})
377
+ await applyDetails(dto)
378
+ toasts.push({ title: 'Approved', message: 'Journal entry was approved.', tone: 'success' })
379
+ } catch (cause) {
380
+ errorMessages.value = extractErrorMessages(cause)
381
+ } finally {
382
+ saving.value = false
383
+ }
384
+ }
385
+
386
+ async function reject() {
387
+ if (!currentId.value) return
388
+ saving.value = true
389
+ errorMessages.value = []
390
+ try {
391
+ const dto = await rejectGeneralJournalEntry(currentId.value, {
392
+ rejectReason: rejectReason.value.trim(),
393
+ })
394
+ await applyDetails(dto)
395
+ toasts.push({ title: 'Rejected', message: 'Journal entry was rejected.', tone: 'warn' })
396
+ } catch (cause) {
397
+ errorMessages.value = extractErrorMessages(cause)
398
+ } finally {
399
+ saving.value = false
400
+ }
401
+ }
402
+
403
+ async function postApproved() {
404
+ if (!currentId.value) return
405
+ saving.value = true
406
+ errorMessages.value = []
407
+ try {
408
+ const dto = await postGeneralJournalEntry(currentId.value, {})
409
+ await applyDetails(dto)
410
+ toasts.push({ title: 'Posted', message: 'Journal entry was posted.', tone: 'success' })
411
+ } catch (cause) {
412
+ errorMessages.value = extractErrorMessages(cause)
413
+ } finally {
414
+ saving.value = false
415
+ }
416
+ }
417
+
418
+ async function reversePosted() {
419
+ if (!currentId.value) return
420
+ saving.value = true
421
+ errorMessages.value = []
422
+ try {
423
+ const reversed = await reverseGeneralJournalEntry(currentId.value, {
424
+ reversalDateUtc: toUtcMidday(reversalDate.value),
425
+ postImmediately: !!reversePostImmediately.value,
426
+ })
427
+
428
+ suppressRouteLoad.value = true
429
+ await router.replace(buildGeneralJournalEntriesPath(reversed.document.id, { basePath: resolvedListPath.value }))
430
+ await applyDetails(reversed)
431
+ toasts.push({ title: 'Reversed', message: 'Reversal journal entry was created.', tone: 'success' })
432
+ } catch (cause) {
433
+ errorMessages.value = extractErrorMessages(cause)
434
+ } finally {
435
+ suppressRouteLoad.value = false
436
+ saving.value = false
437
+ }
438
+ }
439
+
440
+ async function toggleMarkForDeletion() {
441
+ if (!currentId.value) return
442
+ const isRestore = isMarkedForDeletion.value
443
+ saving.value = true
444
+ errorMessages.value = []
445
+ try {
446
+ const dto = isRestore
447
+ ? await unmarkGeneralJournalEntryForDeletion(currentId.value)
448
+ : await markGeneralJournalEntryForDeletion(currentId.value)
449
+
450
+ await applyDetails(dto)
451
+ toasts.push({
452
+ title: isRestore ? 'Restored' : 'Marked for deletion',
453
+ message: isRestore ? 'Draft was restored.' : 'Draft was marked for deletion.',
454
+ tone: isRestore ? 'success' : 'warn',
455
+ })
456
+ } catch (cause) {
457
+ errorMessages.value = extractErrorMessages(cause)
458
+ } finally {
459
+ saving.value = false
460
+ }
461
+ }
462
+
463
+ async function copyShareLink(): Promise<void> {
464
+ if (!currentId.value) return
465
+ await copyAppLink(
466
+ router,
467
+ toasts,
468
+ buildGeneralJournalEntriesPath(currentId.value, { basePath: resolvedListPath.value }),
469
+ )
470
+ }
471
+
472
+ function openAuditLog() {
473
+ if (!canOpenAudit.value) return
474
+ auditOpen.value = true
475
+ }
476
+
477
+ function closeAuditLog() {
478
+ auditOpen.value = false
479
+ }
480
+
481
+ function closePage() {
482
+ void navigateBack(router, route, resolvedListPath.value)
483
+ }
484
+
485
+ function extractErrorMessages(error: unknown): string[] {
486
+ if (error instanceof ApiError) {
487
+ const messages: string[] = []
488
+
489
+ if (error.errorCode === 'gje.lines.required') messages.push('Add at least one journal line.')
490
+ else if (error.errorCode === 'gje.lines.debit_and_credit_required') messages.push('Add at least one debit line and one credit line.')
491
+ else if (error.errorCode === 'gje.lines.unbalanced') {
492
+ const debit = Number(error.context?.debit ?? 0)
493
+ const credit = Number(error.context?.credit ?? 0)
494
+ messages.push(`Journal is unbalanced. Debit ${formatGeneralJournalEntryMoney(debit)} vs Credit ${formatGeneralJournalEntryMoney(credit)}.`)
495
+ } else if (error.errorCode === 'gje.business_field.required') {
496
+ const field = String(error.context?.field ?? 'Field')
497
+ messages.push(`${field} is required.`)
498
+ } else if (error.errorCode === 'gje.line.dimensions.invalid') {
499
+ const lineNo = Number(error.context?.lineNo ?? 0)
500
+ const accountCode = String(error.context?.accountCode ?? '').trim()
501
+ const reason = String(error.context?.reason ?? '').trim().replace(/_/g, ' ')
502
+ const parts = [`Line ${lineNo || '?'} has invalid dimensions.`]
503
+ if (accountCode) parts.push(`Account: ${accountCode}.`)
504
+ if (reason) parts.push(`Reason: ${reason}.`)
505
+ messages.push(parts.join(' '))
506
+ }
507
+
508
+ for (const issue of error.issues ?? []) {
509
+ const message = String(issue.message ?? '').trim()
510
+ if (message) messages.push(message)
511
+ }
512
+
513
+ if (messages.length === 0) {
514
+ const fromErrors = Object.values(error.errors ?? {}).flat().map((x) => String(x ?? '').trim()).filter(Boolean)
515
+ messages.push(...fromErrors)
516
+ }
517
+
518
+ if (messages.length === 0 && error.message) messages.push(error.message)
519
+ return Array.from(new Set(messages.filter(Boolean)))
520
+ }
521
+
522
+ return [error instanceof Error ? error.message : String(error)]
523
+ }
524
+ </script>
525
+
526
+ <template>
527
+ <div data-testid="journal-entry-edit-page" class="flex h-full min-h-0 flex-col bg-ngb-bg">
528
+ <NgbPageHeader :title="pageTitle" can-back @back="navigateBack(router, route, resolvedListPath)">
529
+ <template #secondary>
530
+ <div class="min-w-0 flex flex-wrap items-center gap-2">
531
+ <NgbBadge :tone="documentStatusBadgeTone">{{ documentStatusLabel(documentStatus) }}</NgbBadge>
532
+ <NgbBadge :tone="badgeToneForApproval()">{{ generalJournalEntryApprovalStateLabel(approvalState) }}</NgbBadge>
533
+ <NgbBadge tone="neutral">{{ generalJournalEntrySourceLabel(source) }}</NgbBadge>
534
+ <NgbBadge v-if="sourceDocumentLabel" tone="neutral">Reversal Of: {{ sourceDocumentLabel }}</NgbBadge>
535
+ </div>
536
+ </template>
537
+
538
+ <template #actions>
539
+ <button
540
+ v-if="canShareLink"
541
+ class="ngb-iconbtn"
542
+ :disabled="loading || saving"
543
+ title="Share link"
544
+ @click="copyShareLink"
545
+ >
546
+ <NgbIcon name="share" />
547
+ </button>
548
+
549
+ <button
550
+ v-if="canOpenAudit"
551
+ class="ngb-iconbtn"
552
+ :disabled="loading || saving"
553
+ title="Audit log"
554
+ @click="openAuditLog"
555
+ >
556
+ <NgbIcon name="history" />
557
+ </button>
558
+
559
+ <button
560
+ v-if="canMarkForDeletion || canUnmarkForDeletion"
561
+ class="ngb-iconbtn"
562
+ :disabled="loading || saving"
563
+ :title="canUnmarkForDeletion ? 'Unmark for deletion' : 'Mark for deletion'"
564
+ @click="toggleMarkForDeletion"
565
+ >
566
+ <NgbIcon :name="canUnmarkForDeletion ? 'trash-restore' : 'trash'" />
567
+ </button>
568
+
569
+ <button
570
+ class="ngb-iconbtn"
571
+ :disabled="loading || saving || !canSave"
572
+ title="Save"
573
+ @click="saveDraft"
574
+ >
575
+ <NgbIcon name="save" />
576
+ </button>
577
+
578
+ <button
579
+ v-if="showSubmit"
580
+ class="ngb-iconbtn"
581
+ :disabled="loading || saving || !canSubmit"
582
+ title="Submit"
583
+ @click="submit"
584
+ >
585
+ <NgbIcon name="arrow-right" />
586
+ </button>
587
+
588
+ <button
589
+ v-if="showApprove"
590
+ class="ngb-iconbtn"
591
+ :disabled="loading || saving || !canApprove"
592
+ title="Approve"
593
+ @click="approve"
594
+ >
595
+ <NgbIcon name="shield-check" />
596
+ </button>
597
+
598
+ <button
599
+ v-if="showReject"
600
+ class="ngb-iconbtn"
601
+ :disabled="loading || saving || !canReject || !rejectReason.trim()"
602
+ title="Reject"
603
+ @click="reject"
604
+ >
605
+ <NgbIcon name="circle-x" />
606
+ </button>
607
+
608
+ <button
609
+ v-if="showPost"
610
+ class="ngb-iconbtn"
611
+ :disabled="loading || saving || !canPost"
612
+ title="Post"
613
+ @click="postApproved"
614
+ >
615
+ <NgbIcon name="check" />
616
+ </button>
617
+
618
+ <button
619
+ v-if="showReverse"
620
+ class="ngb-iconbtn"
621
+ :disabled="loading || saving || !canReverse"
622
+ title="Reverse"
623
+ @click="reversePosted"
624
+ >
625
+ <NgbIcon name="undo" />
626
+ </button>
627
+
628
+ <button class="ngb-iconbtn" :disabled="loading || saving" title="Close" @click="closePage">
629
+ <NgbIcon name="x" />
630
+ </button>
631
+ </template>
632
+ </NgbPageHeader>
633
+
634
+ <div class="flex flex-1 min-h-0 overflow-auto overscroll-contain bg-ngb-bg p-6">
635
+ <div class="min-h-full w-full space-y-4 bg-ngb-bg pb-6">
636
+ <div
637
+ v-if="!isNew && isMarkedForDeletion"
638
+ class="flex items-start justify-between gap-3 rounded-[var(--ngb-radius)] border border-red-200 bg-red-50 p-3 dark:border-red-900/50 dark:bg-red-950/30"
639
+ >
640
+ <div class="min-w-0">
641
+ <div class="text-sm font-semibold text-ngb-danger">Deleted</div>
642
+ <div class="mt-1 text-sm text-ngb-muted">
643
+ This document is marked for deletion. Restore it to edit or continue the workflow.
644
+ </div>
645
+ </div>
646
+ </div>
647
+
648
+ <NgbValidationSummary :messages="errorMessages" />
649
+
650
+ <div
651
+ v-if="!isManualSource && !isNew"
652
+ class="rounded-[var(--ngb-radius)] border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900 dark:border-amber-900/50 dark:bg-amber-950/20 dark:text-amber-100"
653
+ >
654
+ This is a system-generated journal entry. Header and lines are read-only.
655
+ </div>
656
+
657
+ <div v-if="loading" class="text-sm text-ngb-muted">Loading…</div>
658
+
659
+ <template v-else>
660
+ <NgbFormLayout>
661
+ <NgbFormSection title="Header" description="General journal entry business fields and workflow state.">
662
+ <NgbFormRow label="Document Date" hint="Date is fixed when the draft is created.">
663
+ <NgbDatePicker v-model="draftDate" :disabled="!isNew || saving" />
664
+ </NgbFormRow>
665
+
666
+ <NgbFormRow label="Initiated By">
667
+ <NgbInput :model-value="initiatedByDisplay" readonly />
668
+ </NgbFormRow>
669
+
670
+ <NgbFormRow label="Journal Type">
671
+ <NgbSelect v-model="journalType" :options="journalTypeOptions" :disabled="!canEditDraft" />
672
+ </NgbFormRow>
673
+
674
+ <NgbFormRow label="Reason Code">
675
+ <NgbInput v-model="reasonCode" :disabled="!canEditDraft" placeholder="Optional business reason code" />
676
+ </NgbFormRow>
677
+
678
+ <NgbFormRow label="Memo">
679
+ <NgbInput v-model="memo" :disabled="!canEditDraft" placeholder="Explain the journal entry" />
680
+ </NgbFormRow>
681
+
682
+ <NgbFormRow label="External Reference">
683
+ <NgbInput v-model="externalReference" :disabled="!canEditDraft" placeholder="External ticket, import id, or source ref" />
684
+ </NgbFormRow>
685
+
686
+ <NgbFormRow label="Auto Reverse">
687
+ <div class="flex h-9 items-center gap-3">
688
+ <NgbCheckbox v-model="autoReverse" :disabled="!canEditDraft" />
689
+ <div class="w-[12rem]">
690
+ <NgbDatePicker v-model="autoReverseOnUtc" :disabled="!canEditDraft || !autoReverse" />
691
+ </div>
692
+ </div>
693
+ </NgbFormRow>
694
+ </NgbFormSection>
695
+ </NgbFormLayout>
696
+
697
+ <NgbTabs v-model="activeTab" :tabs="tabs" full-width-bar>
698
+ <template #default="{ active }">
699
+ <div v-if="active === 'lines'" class="space-y-4 pb-6">
700
+ <NgbGeneralJournalEntryLinesEditor
701
+ v-model="lines"
702
+ :readonly="!canEditDraft"
703
+ :preloaded-account-contexts="preloadedAccountContexts"
704
+ />
705
+
706
+ <div
707
+ v-if="details?.allocations?.length"
708
+ class="overflow-hidden rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card"
709
+ >
710
+ <div class="border-b border-ngb-border px-4 py-3">
711
+ <div class="text-sm font-semibold text-ngb-text">Allocations</div>
712
+ <div class="mt-0.5 text-xs text-ngb-muted">Deterministic debit/credit pairings generated by posting.</div>
713
+ </div>
714
+
715
+ <table class="w-full table-fixed text-sm">
716
+ <thead class="bg-ngb-bg text-xs text-ngb-muted">
717
+ <tr>
718
+ <th class="border-r border-dotted border-ngb-border px-3 py-2 text-left font-semibold">Entry</th>
719
+ <th class="border-r border-dotted border-ngb-border px-3 py-2 text-left font-semibold">Debit Line</th>
720
+ <th class="border-r border-dotted border-ngb-border px-3 py-2 text-left font-semibold">Credit Line</th>
721
+ <th class="px-3 py-2 text-right font-semibold">Amount</th>
722
+ </tr>
723
+ </thead>
724
+ <tbody>
725
+ <tr
726
+ v-for="allocation in details?.allocations ?? []"
727
+ :key="allocation.entryNo"
728
+ class="border-t border-ngb-border transition-colors hover:bg-ngb-bg"
729
+ >
730
+ <td class="border-r border-dotted border-ngb-border px-3 py-2">{{ allocation.entryNo }}</td>
731
+ <td class="border-r border-dotted border-ngb-border px-3 py-2">{{ allocation.debitLineNo }}</td>
732
+ <td class="border-r border-dotted border-ngb-border px-3 py-2">{{ allocation.creditLineNo }}</td>
733
+ <td class="px-3 py-2 text-right">{{ formatGeneralJournalEntryMoney(allocation.amount) }}</td>
734
+ </tr>
735
+ </tbody>
736
+ </table>
737
+ </div>
738
+ </div>
739
+
740
+ <NgbFormLayout v-else class="pb-6">
741
+ <NgbFormSection title="Workflow" description="Workflow actors are captured automatically from the authenticated current user.">
742
+ <NgbFormRow label="Submitted By">
743
+ <NgbInput :model-value="submittedByDisplay" readonly />
744
+ </NgbFormRow>
745
+
746
+ <NgbFormRow label="Approved By">
747
+ <NgbInput :model-value="approvedByDisplay" readonly />
748
+ </NgbFormRow>
749
+
750
+ <NgbFormRow label="Rejected By">
751
+ <NgbInput :model-value="rejectedByDisplay" readonly />
752
+ </NgbFormRow>
753
+
754
+ <NgbFormRow label="Reject Reason">
755
+ <NgbInput v-model="rejectReason" :disabled="!canReject" placeholder="Required when rejecting" />
756
+ </NgbFormRow>
757
+
758
+ <NgbFormRow label="Posted By">
759
+ <NgbInput :model-value="postedByDisplay" readonly />
760
+ </NgbFormRow>
761
+
762
+ <NgbFormRow label="Reverse Initiated By">
763
+ <NgbInput :model-value="reverseInitiatedByDisplay" readonly />
764
+ </NgbFormRow>
765
+
766
+ <NgbFormRow label="Reversal Date">
767
+ <div class="flex items-center gap-3">
768
+ <div class="w-[12rem]">
769
+ <NgbDatePicker v-model="reversalDate" :disabled="!canReverse" />
770
+ </div>
771
+ <div class="flex h-9 items-center gap-2">
772
+ <NgbCheckbox v-model="reversePostImmediately" :disabled="!canReverse" />
773
+ <span class="text-sm text-ngb-muted">Post immediately</span>
774
+ </div>
775
+ </div>
776
+ </NgbFormRow>
777
+ </NgbFormSection>
778
+ </NgbFormLayout>
779
+ </template>
780
+ </NgbTabs>
781
+ </template>
782
+ </div>
783
+ </div>
784
+
785
+ <NgbDrawer v-model:open="auditOpen" title="Audit Log" hide-header flush-body>
786
+ <NgbEntityAuditSidebar
787
+ :open="auditOpen"
788
+ :entity-kind="auditEntityKind"
789
+ :entity-id="auditEntityId"
790
+ :entity-title="auditEntityTitle"
791
+ @back="closeAuditLog"
792
+ @close="closeAuditLog"
793
+ />
794
+ </NgbDrawer>
795
+ </div>
796
+ </template>