@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,590 @@
1
+ <script setup lang="ts">
2
+ import { computed, reactive, ref, watch } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+
5
+ import NgbConfirmDialog from '../components/NgbConfirmDialog.vue'
6
+ import NgbFormLayout from '../components/forms/NgbFormLayout.vue'
7
+ import NgbFormRow from '../components/forms/NgbFormRow.vue'
8
+ import NgbValidationSummary from '../components/forms/NgbValidationSummary.vue'
9
+ import NgbEntityAuditSidebar from '../editor/NgbEntityAuditSidebar.vue'
10
+ import type { EntityEditorFlags } from '../editor/types'
11
+ import NgbCheckbox from '../primitives/NgbCheckbox.vue'
12
+ import NgbInput from '../primitives/NgbInput.vue'
13
+ import NgbSelect from '../primitives/NgbSelect.vue'
14
+ import { useToasts } from '../primitives/toast'
15
+ import { copyAppLink } from '../router/shareLink'
16
+ import { toErrorMessage } from '../utils/errorMessage'
17
+ import { stableStringify } from '../utils/stableValue'
18
+ import {
19
+ createChartOfAccount,
20
+ getChartOfAccountById,
21
+ markChartOfAccountForDeletion,
22
+ unmarkChartOfAccountForDeletion,
23
+ updateChartOfAccount,
24
+ } from './api'
25
+ import { buildChartOfAccountsPath } from './navigation'
26
+ import type {
27
+ ChartOfAccountsAccountDto,
28
+ ChartOfAccountsCashFlowRoleOptionDto,
29
+ ChartOfAccountsMetadataDto,
30
+ ChartOfAccountsUpsertRequestDto,
31
+ ChartOfAccountEditorShellState,
32
+ } from './types'
33
+
34
+ type ChartOfAccountFieldErrors = {
35
+ code?: string
36
+ name?: string
37
+ accountType?: string
38
+ cashFlowLineCode?: string
39
+ }
40
+
41
+ const AUDIT_ENTITY_KIND_COA_ACCOUNT = 3
42
+ const DEFAULT_EDITOR_SHELL: ChartOfAccountEditorShellState = {
43
+ hideHeader: false,
44
+ flushBody: false,
45
+ }
46
+
47
+ const props = withDefaults(defineProps<{
48
+ id?: string | null
49
+ metadata?: ChartOfAccountsMetadataDto | null
50
+ routeBasePath?: string | null
51
+ }>(), {
52
+ id: null,
53
+ metadata: null,
54
+ routeBasePath: null,
55
+ })
56
+
57
+ const emit = defineEmits<{
58
+ (e: 'created', id: string): void
59
+ (e: 'saved'): void
60
+ (e: 'changed'): void
61
+ (e: 'state', value: { title: string; subtitle?: string }): void
62
+ (e: 'flags', value: EntityEditorFlags): void
63
+ (e: 'shell', value: ChartOfAccountEditorShellState): void
64
+ (e: 'close'): void
65
+ }>()
66
+
67
+ const router = useRouter()
68
+ const route = useRoute()
69
+ const toasts = useToasts()
70
+
71
+ const loading = ref(false)
72
+ const saving = ref(false)
73
+ const error = ref<string | null>(null)
74
+ const current = ref<ChartOfAccountsAccountDto | null>(null)
75
+ const auditOpen = ref(false)
76
+ const markConfirmOpen = ref(false)
77
+
78
+ const form = reactive<ChartOfAccountsUpsertRequestDto>({
79
+ code: '',
80
+ name: '',
81
+ accountType: 'Asset',
82
+ isActive: true,
83
+ cashFlowRole: null,
84
+ cashFlowLineCode: null,
85
+ })
86
+
87
+ const defaultAccountType = computed(() => props.metadata?.accountTypeOptions[0]?.value ?? 'Asset')
88
+ const isEditMode = computed(() => !!props.id)
89
+ const isMetadataReady = computed(() => !!props.metadata)
90
+ const isMarkedForDeletion = computed(() => !!(current.value?.isDeleted || current.value?.isMarkedForDeletion))
91
+ const isReadOnly = computed(() => isEditMode.value && isMarkedForDeletion.value)
92
+ const resolvedRouteBasePath = computed(() => String(props.routeBasePath ?? '').trim() || route.path)
93
+
94
+ const typeOptions = computed(() =>
95
+ (props.metadata?.accountTypeOptions ?? []).map((option) => ({
96
+ value: option.value,
97
+ label: option.label,
98
+ })),
99
+ )
100
+
101
+ const cashFlowRoleOptions = computed(() =>
102
+ (props.metadata?.cashFlowRoleOptions ?? []).map((option) => ({
103
+ value: option.value,
104
+ label: option.label,
105
+ })),
106
+ )
107
+
108
+ const selectedCashFlowRoleOption = computed<ChartOfAccountsCashFlowRoleOptionDto | null>(() => {
109
+ const selected = String(form.cashFlowRole ?? '').trim()
110
+ if (!selected) return null
111
+ return props.metadata?.cashFlowRoleOptions.find((option) => option.value === selected) ?? null
112
+ })
113
+
114
+ const compatibleCashFlowLineOptions = computed(() => {
115
+ const selectedRole = String(form.cashFlowRole ?? '').trim()
116
+ if (!selectedRole) return []
117
+
118
+ return (props.metadata?.cashFlowLineOptions ?? [])
119
+ .filter((option) => option.allowedRoles.includes(selectedRole))
120
+ .map((option) => ({ value: option.value, label: option.label }))
121
+ })
122
+
123
+ const cashFlowLineOptions = computed(() => {
124
+ const role = selectedCashFlowRoleOption.value
125
+ if (!role?.supportsLineCode) return [{ value: '', label: 'Not applicable' }]
126
+
127
+ const prefix = role.requiresLineCode
128
+ ? [{ value: '', label: 'Select line…' }]
129
+ : [{ value: '', label: 'None' }]
130
+
131
+ return [...prefix, ...compatibleCashFlowLineOptions.value]
132
+ })
133
+
134
+ const cashFlowLineDisabled = computed(() =>
135
+ loading.value
136
+ || saving.value
137
+ || isReadOnly.value
138
+ || !selectedCashFlowRoleOption.value?.supportsLineCode,
139
+ )
140
+
141
+ const codeValue = computed(() => String(form.code ?? '').trim())
142
+ const nameValue = computed(() => String(form.name ?? '').trim())
143
+ const accountTypeValue = computed(() => String(form.accountType ?? '').trim())
144
+ const cashFlowLineCodeValue = computed(() => String(form.cashFlowLineCode ?? '').trim())
145
+
146
+ function normalizeOptionalText(value: string | null | undefined): string | null {
147
+ const normalized = String(value ?? '').trim()
148
+ return normalized.length > 0 ? normalized : null
149
+ }
150
+
151
+ function resetForm() {
152
+ form.code = ''
153
+ form.name = ''
154
+ form.accountType = defaultAccountType.value
155
+ form.isActive = true
156
+ form.cashFlowRole = null
157
+ form.cashFlowLineCode = null
158
+ }
159
+
160
+ function applyAccount(account: ChartOfAccountsAccountDto) {
161
+ form.code = account.code
162
+ form.name = account.name
163
+ form.accountType = account.accountType
164
+ form.isActive = account.isActive
165
+ form.cashFlowRole = normalizeOptionalText(account.cashFlowRole)
166
+ form.cashFlowLineCode = normalizeOptionalText(account.cashFlowLineCode)
167
+ }
168
+
169
+ function buildRequest(): ChartOfAccountsUpsertRequestDto {
170
+ const role = normalizeOptionalText(form.cashFlowRole)
171
+ const lineCode = selectedCashFlowRoleOption.value?.supportsLineCode
172
+ ? normalizeOptionalText(form.cashFlowLineCode)
173
+ : null
174
+
175
+ return {
176
+ code: codeValue.value,
177
+ name: nameValue.value,
178
+ accountType: accountTypeValue.value || defaultAccountType.value,
179
+ isActive: !!form.isActive,
180
+ cashFlowRole: role,
181
+ cashFlowLineCode: lineCode,
182
+ }
183
+ }
184
+
185
+ const currentSnapshot = computed(() => stableStringify(buildRequest()))
186
+ const initialSnapshot = ref('')
187
+ const isDirty = computed(() => initialSnapshot.value !== '' && currentSnapshot.value !== initialSnapshot.value)
188
+
189
+ const fieldErrors = computed<ChartOfAccountFieldErrors>(() => {
190
+ const next: ChartOfAccountFieldErrors = {}
191
+
192
+ if (!codeValue.value) next.code = 'Code is required.'
193
+ if (!nameValue.value) next.name = 'Name is required.'
194
+ if (!accountTypeValue.value) next.accountType = 'Type is required.'
195
+ if (selectedCashFlowRoleOption.value?.requiresLineCode && !cashFlowLineCodeValue.value) {
196
+ next.cashFlowLineCode = 'Cash Flow Line Code is required for the selected role.'
197
+ }
198
+
199
+ return next
200
+ })
201
+
202
+ const validationMessages = computed(() =>
203
+ Object.values(fieldErrors.value).filter((message): message is string => typeof message === 'string' && message.length > 0),
204
+ )
205
+
206
+ const canSave = computed(() =>
207
+ isMetadataReady.value
208
+ && !loading.value
209
+ && !saving.value
210
+ && validationMessages.value.length === 0
211
+ && (!isEditMode.value || !isMarkedForDeletion.value),
212
+ )
213
+
214
+ const title = computed(() => {
215
+ if (!isEditMode.value) return 'New account'
216
+
217
+ const code = String(current.value?.code ?? '').trim()
218
+ const name = String(current.value?.name ?? '').trim()
219
+ if (code && name) return `${code} — ${name}`
220
+ return code || name || 'Account'
221
+ })
222
+
223
+ const auditEntityTitle = computed(() => {
224
+ const code = String(current.value?.code ?? '').trim()
225
+ const name = String(current.value?.name ?? '').trim()
226
+ if (code && name) return `${code} — ${name}`
227
+ return code || name || null
228
+ })
229
+
230
+ const flags = computed<EntityEditorFlags>(() => ({
231
+ canSave: canSave.value,
232
+ isDirty: isDirty.value,
233
+ loading: loading.value || !isMetadataReady.value,
234
+ saving: saving.value,
235
+ canExpand: false,
236
+ canDelete: false,
237
+ canMarkForDeletion: isEditMode.value && !isMarkedForDeletion.value && !loading.value && !saving.value,
238
+ canUnmarkForDeletion: isEditMode.value && isMarkedForDeletion.value && !loading.value && !saving.value,
239
+ canPost: false,
240
+ canUnpost: false,
241
+ canShowAudit: isEditMode.value && !!current.value?.accountId,
242
+ canShareLink: isEditMode.value && !!current.value?.accountId,
243
+ }))
244
+
245
+ watch(
246
+ () => title.value,
247
+ () => emit('state', { title: title.value }),
248
+ { immediate: true },
249
+ )
250
+
251
+ watch(
252
+ () => flags.value,
253
+ (value) => emit('flags', value),
254
+ { immediate: true },
255
+ )
256
+
257
+ watch(
258
+ () => auditOpen.value,
259
+ (open) => emit('shell', open ? { hideHeader: true, flushBody: true } : DEFAULT_EDITOR_SHELL),
260
+ { immediate: true },
261
+ )
262
+
263
+ watch(
264
+ () => [
265
+ selectedCashFlowRoleOption.value?.supportsLineCode ?? false,
266
+ compatibleCashFlowLineOptions.value.map((option) => option.value).join('|'),
267
+ ],
268
+ ([supportsLineCode]) => {
269
+ if (!supportsLineCode) {
270
+ form.cashFlowLineCode = null
271
+ return
272
+ }
273
+
274
+ if (
275
+ form.cashFlowLineCode
276
+ && !compatibleCashFlowLineOptions.value.some((option) => option.value === form.cashFlowLineCode)
277
+ ) {
278
+ form.cashFlowLineCode = null
279
+ }
280
+ },
281
+ )
282
+
283
+ watch(
284
+ () => typeOptions.value.map((option) => option.value).join('|'),
285
+ () => {
286
+ if (current.value) return
287
+ if (typeOptions.value.length === 0) return
288
+ if (!typeOptions.value.some((option) => option.value === form.accountType)) {
289
+ form.accountType = defaultAccountType.value
290
+ }
291
+ if (!isDirty.value) {
292
+ initialSnapshot.value = currentSnapshot.value
293
+ }
294
+ },
295
+ { immediate: true },
296
+ )
297
+
298
+ let loadSeq = 0
299
+
300
+ function initializeCreateMode() {
301
+ loadSeq += 1
302
+ loading.value = false
303
+ error.value = null
304
+ current.value = null
305
+ auditOpen.value = false
306
+ resetForm()
307
+ initialSnapshot.value = currentSnapshot.value
308
+ }
309
+
310
+ async function loadAccount(accountId: string) {
311
+ const seq = ++loadSeq
312
+ loading.value = true
313
+ error.value = null
314
+ current.value = null
315
+ auditOpen.value = false
316
+
317
+ try {
318
+ const account = await getChartOfAccountById(accountId)
319
+ if (seq !== loadSeq) return
320
+ current.value = account
321
+ applyAccount(account)
322
+ initialSnapshot.value = currentSnapshot.value
323
+ } catch (cause) {
324
+ if (seq !== loadSeq) return
325
+ error.value = toErrorMessage(cause, 'Failed to load the account.')
326
+ } finally {
327
+ if (seq === loadSeq) loading.value = false
328
+ }
329
+ }
330
+
331
+ watch(
332
+ () => props.id,
333
+ (nextId) => {
334
+ if (!nextId) {
335
+ initializeCreateMode()
336
+ return
337
+ }
338
+
339
+ void loadAccount(nextId)
340
+ },
341
+ { immediate: true },
342
+ )
343
+
344
+ function buildAccountShareTarget(accountId: string) {
345
+ return buildChartOfAccountsPath({
346
+ panel: 'edit',
347
+ id: accountId,
348
+ basePath: resolvedRouteBasePath.value,
349
+ })
350
+ }
351
+
352
+ async function copyShareLink() {
353
+ const accountId = current.value?.accountId
354
+ if (!accountId) return false
355
+
356
+ return await copyAppLink(
357
+ router,
358
+ toasts,
359
+ buildAccountShareTarget(accountId),
360
+ { message: 'Shareable account link copied to clipboard.' },
361
+ )
362
+ }
363
+
364
+ function openAuditLog() {
365
+ if (!current.value?.accountId) return
366
+ auditOpen.value = true
367
+ }
368
+
369
+ function closeAuditLog() {
370
+ auditOpen.value = false
371
+ }
372
+
373
+ function requestMarkForDeletion() {
374
+ if (!flags.value.canMarkForDeletion) return
375
+ markConfirmOpen.value = true
376
+ }
377
+
378
+ function cancelMarkForDeletion() {
379
+ markConfirmOpen.value = false
380
+ }
381
+
382
+ async function save() {
383
+ if (!canSave.value) return
384
+
385
+ saving.value = true
386
+ error.value = null
387
+
388
+ try {
389
+ const request = buildRequest()
390
+
391
+ if (!isEditMode.value) {
392
+ const created = await createChartOfAccount(request)
393
+ current.value = created
394
+ applyAccount(created)
395
+ initialSnapshot.value = currentSnapshot.value
396
+ emit('created', created.accountId)
397
+ return
398
+ }
399
+
400
+ const accountId = current.value?.accountId ?? props.id
401
+ if (!accountId) throw new Error('Missing accountId')
402
+
403
+ const updated = await updateChartOfAccount(accountId, request)
404
+ current.value = updated
405
+ applyAccount(updated)
406
+ initialSnapshot.value = currentSnapshot.value
407
+ emit('saved')
408
+ } catch (cause) {
409
+ error.value = toErrorMessage(cause, 'Failed to save the account.')
410
+ } finally {
411
+ saving.value = false
412
+ }
413
+ }
414
+
415
+ async function markForDeletion() {
416
+ const accountId = current.value?.accountId
417
+ if (!accountId || saving.value) return
418
+
419
+ saving.value = true
420
+ error.value = null
421
+
422
+ try {
423
+ await markChartOfAccountForDeletion(accountId)
424
+ current.value = current.value
425
+ ? {
426
+ ...current.value,
427
+ isMarkedForDeletion: true,
428
+ }
429
+ : null
430
+ auditOpen.value = false
431
+ emit('changed')
432
+ } catch (cause) {
433
+ error.value = toErrorMessage(cause, 'Failed to mark the account for deletion.')
434
+ } finally {
435
+ saving.value = false
436
+ }
437
+ }
438
+
439
+ async function confirmMarkForDeletion() {
440
+ markConfirmOpen.value = false
441
+ await markForDeletion()
442
+ }
443
+
444
+ async function unmarkForDeletion() {
445
+ const accountId = current.value?.accountId
446
+ if (!accountId || saving.value) return
447
+
448
+ saving.value = true
449
+ error.value = null
450
+
451
+ try {
452
+ await unmarkChartOfAccountForDeletion(accountId)
453
+ current.value = current.value
454
+ ? {
455
+ ...current.value,
456
+ isMarkedForDeletion: false,
457
+ isDeleted: false,
458
+ }
459
+ : null
460
+ auditOpen.value = false
461
+ emit('changed')
462
+ } catch (cause) {
463
+ error.value = toErrorMessage(cause, 'Failed to restore the account.')
464
+ } finally {
465
+ saving.value = false
466
+ }
467
+ }
468
+
469
+ function toggleMarkForDeletion() {
470
+ if (flags.value.canUnmarkForDeletion) {
471
+ void unmarkForDeletion()
472
+ return
473
+ }
474
+
475
+ if (flags.value.canMarkForDeletion) requestMarkForDeletion()
476
+ }
477
+
478
+ defineExpose({
479
+ save,
480
+ copyShareLink,
481
+ openAuditLog,
482
+ closeAuditLog,
483
+ toggleMarkForDeletion,
484
+ })
485
+ </script>
486
+
487
+ <template>
488
+ <NgbEntityAuditSidebar
489
+ v-if="auditOpen"
490
+ :open="auditOpen"
491
+ :entity-kind="AUDIT_ENTITY_KIND_COA_ACCOUNT"
492
+ :entity-id="current?.accountId ?? null"
493
+ :entity-title="auditEntityTitle"
494
+ @back="closeAuditLog"
495
+ @close="emit('close')"
496
+ />
497
+
498
+ <template v-else>
499
+ <div v-if="error" class="mb-4 rounded-[var(--ngb-radius)] border border-red-200 bg-red-50 p-3 text-sm text-red-900 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-100">
500
+ {{ error }}
501
+ </div>
502
+
503
+ <NgbValidationSummary
504
+ v-if="validationMessages.length > 0 && !loading && isMetadataReady"
505
+ :messages="validationMessages"
506
+ />
507
+
508
+ <div v-if="loading || !isMetadataReady" class="mt-4 text-sm text-ngb-muted">
509
+ Loading…
510
+ </div>
511
+
512
+ <div v-else data-testid="chart-of-account-editor" class="mt-4">
513
+ <div
514
+ v-if="isEditMode && isMarkedForDeletion"
515
+ class="mb-4 rounded-[var(--ngb-radius)] border border-red-200 bg-red-50 p-3 text-sm dark:border-red-900/50 dark:bg-red-950/30"
516
+ >
517
+ <div class="font-semibold text-ngb-danger">Deleted</div>
518
+ <div class="mt-1 text-ngb-muted">This account is marked for deletion. Restore it to edit.</div>
519
+ </div>
520
+
521
+ <NgbFormLayout>
522
+ <NgbFormRow label="Code" :error="fieldErrors.code">
523
+ <NgbInput
524
+ v-model="form.code"
525
+ placeholder="e.g. 1000"
526
+ :disabled="saving || isReadOnly"
527
+ />
528
+ </NgbFormRow>
529
+
530
+ <NgbFormRow label="Name" :error="fieldErrors.name">
531
+ <NgbInput
532
+ v-model="form.name"
533
+ placeholder="e.g. Cash"
534
+ :disabled="saving || isReadOnly"
535
+ />
536
+ </NgbFormRow>
537
+
538
+ <NgbFormRow label="Type" :error="fieldErrors.accountType">
539
+ <NgbSelect
540
+ v-model="form.accountType"
541
+ :options="typeOptions"
542
+ :disabled="saving || isReadOnly"
543
+ />
544
+ </NgbFormRow>
545
+
546
+ <NgbFormRow label="Active">
547
+ <div class="flex h-9 items-center">
548
+ <NgbCheckbox
549
+ v-model="form.isActive"
550
+ :disabled="saving || isReadOnly"
551
+ />
552
+ </div>
553
+ </NgbFormRow>
554
+
555
+ <NgbFormRow label="Cash Flow Role">
556
+ <NgbSelect
557
+ v-model="form.cashFlowRole"
558
+ :options="cashFlowRoleOptions"
559
+ :disabled="saving || isReadOnly"
560
+ />
561
+ </NgbFormRow>
562
+
563
+ <NgbFormRow
564
+ label="Cash Flow Line Code"
565
+ :hint="selectedCashFlowRoleOption?.requiresLineCode ? 'Required for the selected role.' : undefined"
566
+ :error="fieldErrors.cashFlowLineCode"
567
+ >
568
+ <NgbSelect
569
+ v-model="form.cashFlowLineCode"
570
+ :options="cashFlowLineOptions"
571
+ :disabled="cashFlowLineDisabled"
572
+ />
573
+ </NgbFormRow>
574
+ </NgbFormLayout>
575
+ </div>
576
+ </template>
577
+
578
+ <NgbConfirmDialog
579
+ :open="markConfirmOpen"
580
+ title="Mark for deletion?"
581
+ :message="isDirty
582
+ ? 'This account will be marked for deletion. Unsaved changes will be lost.'
583
+ : 'This account will be marked for deletion. You can restore it later.'"
584
+ confirm-text="Mark"
585
+ cancel-text="Cancel"
586
+ danger
587
+ @update:open="(value) => (!value ? cancelMarkForDeletion() : null)"
588
+ @confirm="confirmMarkForDeletion"
589
+ />
590
+ </template>