@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.
- package/LICENSE +201 -0
- package/README.md +44 -0
- package/package.json +61 -0
- package/public/favicon.svg +6 -0
- package/public/silent-check-sso.html +8 -0
- package/src/assets/ngb_logo.svg +17 -0
- package/src/assets/ngb_logo_color.svg +17 -0
- package/src/index.ts +1023 -0
- package/src/ngb/README.md +27 -0
- package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
- package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
- package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
- package/src/ngb/accounting/api.ts +79 -0
- package/src/ngb/accounting/generalJournalEntry.ts +127 -0
- package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
- package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
- package/src/ngb/accounting/navigation.ts +83 -0
- package/src/ngb/accounting/periodClosing.ts +75 -0
- package/src/ngb/accounting/periodClosingApi.ts +60 -0
- package/src/ngb/accounting/periodClosingTypes.ts +73 -0
- package/src/ngb/accounting/types.ts +57 -0
- package/src/ngb/api/audit.ts +23 -0
- package/src/ngb/api/catalogs.ts +50 -0
- package/src/ngb/api/contracts.ts +285 -0
- package/src/ngb/api/documents.ts +170 -0
- package/src/ngb/api/http.ts +446 -0
- package/src/ngb/api/lookups.ts +14 -0
- package/src/ngb/api/types.ts +6 -0
- package/src/ngb/auth/index.ts +13 -0
- package/src/ngb/auth/keycloak.ts +374 -0
- package/src/ngb/auth/router.ts +35 -0
- package/src/ngb/auth/types.ts +12 -0
- package/src/ngb/auth/useAuthStore.ts +209 -0
- package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
- package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
- package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
- package/src/ngb/command-palette/api.ts +10 -0
- package/src/ngb/command-palette/config.ts +16 -0
- package/src/ngb/command-palette/presentation.ts +85 -0
- package/src/ngb/command-palette/search.ts +130 -0
- package/src/ngb/command-palette/staticItems.ts +243 -0
- package/src/ngb/command-palette/storage.ts +45 -0
- package/src/ngb/command-palette/store.ts +716 -0
- package/src/ngb/command-palette/types.ts +120 -0
- package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
- package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
- package/src/ngb/components/NgbConfirmDialog.vue +63 -0
- package/src/ngb/components/NgbDialog.vue +44 -0
- package/src/ngb/components/NgbDrawer.vue +156 -0
- package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
- package/src/ngb/components/NgbModalShell.vue +99 -0
- package/src/ngb/components/NgbToolbar.vue +14 -0
- package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
- package/src/ngb/components/forms/NgbFormRow.vue +21 -0
- package/src/ngb/components/forms/NgbFormSection.vue +16 -0
- package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
- package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
- package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
- package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
- package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
- package/src/ngb/components/register/registerTypes.ts +51 -0
- package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
- package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
- package/src/ngb/components/register/useRegisterRows.ts +209 -0
- package/src/ngb/components/register/useRegisterViewport.ts +56 -0
- package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
- package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
- package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
- package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
- package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
- package/src/ngb/editor/NgbEntityEditor.vue +250 -0
- package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
- package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
- package/src/ngb/editor/catalogNavigation.ts +13 -0
- package/src/ngb/editor/config.ts +252 -0
- package/src/ngb/editor/documentCopyDraft.ts +245 -0
- package/src/ngb/editor/documentEffects.ts +43 -0
- package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
- package/src/ngb/editor/documentNavigation.ts +115 -0
- package/src/ngb/editor/documentStatus.ts +45 -0
- package/src/ngb/editor/entityEditorErrors.ts +162 -0
- package/src/ngb/editor/entityEditorPersistence.ts +241 -0
- package/src/ngb/editor/extensions.ts +40 -0
- package/src/ngb/editor/types.ts +295 -0
- package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
- package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
- package/src/ngb/editor/useEditorDrawerState.ts +88 -0
- package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
- package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
- package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
- package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
- package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
- package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
- package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
- package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
- package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
- package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
- package/src/ngb/env/runtimeConfig.ts +25 -0
- package/src/ngb/lookup/config.ts +42 -0
- package/src/ngb/lookup/defaultConfig.ts +136 -0
- package/src/ngb/lookup/navigation.ts +76 -0
- package/src/ngb/lookup/prefetch.ts +40 -0
- package/src/ngb/lookup/store.ts +261 -0
- package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
- package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
- package/src/ngb/metadata/NgbEntityForm.vue +84 -0
- package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
- package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
- package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
- package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
- package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
- package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
- package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
- package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
- package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
- package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
- package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
- package/src/ngb/metadata/config.ts +28 -0
- package/src/ngb/metadata/dataTypes.ts +27 -0
- package/src/ngb/metadata/entityForm.ts +114 -0
- package/src/ngb/metadata/entityModel.ts +34 -0
- package/src/ngb/metadata/entityValueFormatting.ts +83 -0
- package/src/ngb/metadata/fieldRendererState.ts +84 -0
- package/src/ngb/metadata/filtering.ts +153 -0
- package/src/ngb/metadata/lookup.ts +25 -0
- package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
- package/src/ngb/metadata/normalization.ts +100 -0
- package/src/ngb/metadata/referenceHydration.ts +99 -0
- package/src/ngb/metadata/register.ts +135 -0
- package/src/ngb/metadata/routePages.ts +86 -0
- package/src/ngb/metadata/store.ts +35 -0
- package/src/ngb/metadata/types.ts +276 -0
- package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
- package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
- package/src/ngb/metadata/useValidationFocus.ts +49 -0
- package/src/ngb/primitives/NgbBadge.vue +31 -0
- package/src/ngb/primitives/NgbButton.vue +53 -0
- package/src/ngb/primitives/NgbCheckbox.vue +33 -0
- package/src/ngb/primitives/NgbDatePicker.vue +141 -0
- package/src/ngb/primitives/NgbIcon.vue +545 -0
- package/src/ngb/primitives/NgbInput.vue +57 -0
- package/src/ngb/primitives/NgbLookup.vue +264 -0
- package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
- package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
- package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
- package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
- package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
- package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
- package/src/ngb/primitives/NgbSelect.vue +108 -0
- package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
- package/src/ngb/primitives/NgbSwitch.vue +42 -0
- package/src/ngb/primitives/NgbTabs.vue +116 -0
- package/src/ngb/primitives/NgbToastHost.vue +49 -0
- package/src/ngb/primitives/NgbTooltip.vue +27 -0
- package/src/ngb/primitives/iconNames.ts +83 -0
- package/src/ngb/primitives/toast.ts +66 -0
- package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
- package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
- package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
- package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
- package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
- package/src/ngb/reporting/NgbReportPage.vue +1124 -0
- package/src/ngb/reporting/NgbReportSheet.vue +467 -0
- package/src/ngb/reporting/api.ts +35 -0
- package/src/ngb/reporting/composer.ts +904 -0
- package/src/ngb/reporting/config.ts +120 -0
- package/src/ngb/reporting/defaultConfig.ts +15 -0
- package/src/ngb/reporting/lookupFilters.ts +43 -0
- package/src/ngb/reporting/navigation.ts +181 -0
- package/src/ngb/reporting/pageHelpers.ts +73 -0
- package/src/ngb/reporting/pageSession.ts +92 -0
- package/src/ngb/reporting/paging.ts +66 -0
- package/src/ngb/reporting/types.ts +333 -0
- package/src/ngb/router/backNavigation.ts +122 -0
- package/src/ngb/router/queryParams.ts +129 -0
- package/src/ngb/router/queryState.ts +121 -0
- package/src/ngb/router/routeAliases.ts +66 -0
- package/src/ngb/router/routeParams.ts +15 -0
- package/src/ngb/router/shareLink.ts +39 -0
- package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
- package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
- package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
- package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
- package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
- package/src/ngb/security/NgbRolesPage.vue +125 -0
- package/src/ngb/security/NgbUserEditorPage.vue +615 -0
- package/src/ngb/security/NgbUsersPage.vue +125 -0
- package/src/ngb/security/api.ts +91 -0
- package/src/ngb/security/audit.ts +21 -0
- package/src/ngb/security/permissions.ts +65 -0
- package/src/ngb/security/types.ts +157 -0
- package/src/ngb/security/useAccessStore.ts +69 -0
- package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
- package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
- package/src/ngb/site/NgbLogo.vue +44 -0
- package/src/ngb/site/NgbPageHeader.vue +49 -0
- package/src/ngb/site/NgbSiteShell.vue +286 -0
- package/src/ngb/site/NgbSiteSidebar.vue +270 -0
- package/src/ngb/site/NgbTopBar.vue +425 -0
- package/src/ngb/site/NgbTrendChart.vue +224 -0
- package/src/ngb/site/dashboardData.ts +324 -0
- package/src/ngb/site/mainMenuStore.ts +49 -0
- package/src/ngb/site/types.ts +31 -0
- package/src/ngb/site/useDashboardPageState.ts +98 -0
- package/src/ngb/site/useTheme.ts +95 -0
- package/src/ngb/utils/clone.ts +43 -0
- package/src/ngb/utils/dateValues.ts +107 -0
- package/src/ngb/utils/errorMessage.ts +14 -0
- package/src/ngb/utils/guid.ts +21 -0
- package/src/ngb/utils/stableValue.ts +21 -0
- package/src/ngb/utils/storage.ts +135 -0
- package/src/styles/tailwind.css +165 -0
- package/src/vite/publicAssets.ts +104 -0
- 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>
|