@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,454 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+
5
+ import NgbBadge from '../primitives/NgbBadge.vue'
6
+ import NgbIcon from '../primitives/NgbIcon.vue'
7
+ import NgbInput from '../primitives/NgbInput.vue'
8
+ import NgbLookup from '../primitives/NgbLookup.vue'
9
+ import NgbSelect from '../primitives/NgbSelect.vue'
10
+ import { useLookupStore } from '../lookup/store'
11
+ import { buildLookupFieldTargetUrl } from '../lookup/navigation'
12
+ import { getGeneralJournalEntryAccountContext } from './generalJournalEntryApi'
13
+ import {
14
+ createGeneralJournalEntryLine,
15
+ formatGeneralJournalEntryMoney,
16
+ parseGeneralJournalEntryAmount,
17
+ } from './generalJournalEntry'
18
+ import type {
19
+ GeneralJournalEntryAccountContextDto,
20
+ GeneralJournalEntryDimensionRuleDto,
21
+ GeneralJournalEntryEditorLineModel,
22
+ } from './generalJournalEntryTypes'
23
+
24
+ const props = withDefaults(
25
+ defineProps<{
26
+ modelValue: GeneralJournalEntryEditorLineModel[]
27
+ readonly?: boolean
28
+ preloadedAccountContexts?: Record<string, GeneralJournalEntryAccountContextDto | null>
29
+ }>(),
30
+ {
31
+ readonly: false,
32
+ preloadedAccountContexts: () => ({}),
33
+ },
34
+ )
35
+
36
+ const emit = defineEmits<{
37
+ (e: 'update:modelValue', value: GeneralJournalEntryEditorLineModel[]): void
38
+ }>()
39
+
40
+ const lookupStore = useLookupStore()
41
+ const router = useRouter()
42
+ const route = useRoute()
43
+
44
+ const accountItemsByRow = ref<Record<string, GeneralJournalEntryEditorLineModel['account'][]>>({})
45
+ const dimensionItemsByCell = ref<Record<string, GeneralJournalEntryEditorLineModel['account'][]>>({})
46
+ const accountContextsByRow = ref<Record<string, GeneralJournalEntryAccountContextDto | null>>({})
47
+ const accountContextCache = ref<Record<string, GeneralJournalEntryAccountContextDto | null>>({})
48
+ const loadingContexts = ref<Record<string, string>>({})
49
+
50
+ const rows = computed(() => props.modelValue ?? [])
51
+ const canEdit = computed(() => !props.readonly)
52
+
53
+ const sideOptions = [
54
+ { value: 1, label: 'Debit' },
55
+ { value: 2, label: 'Credit' },
56
+ ]
57
+
58
+ const totals = computed(() => {
59
+ let debit = 0
60
+ let credit = 0
61
+ for (const row of rows.value) {
62
+ const amount = parseGeneralJournalEntryAmount(row.amount)
63
+ if (!Number.isFinite(amount)) continue
64
+ if (row.side === 1) debit += amount
65
+ else credit += amount
66
+ }
67
+ return {
68
+ debit,
69
+ credit,
70
+ diff: debit - credit,
71
+ }
72
+ })
73
+
74
+ function emitRows(next: GeneralJournalEntryEditorLineModel[]) {
75
+ emit('update:modelValue', next)
76
+ }
77
+
78
+ function updateRow(rowIndex: number, patch: Partial<GeneralJournalEntryEditorLineModel>) {
79
+ const next = rows.value.map((row, index) => {
80
+ if (index !== rowIndex) return { ...row, dimensions: { ...(row.dimensions ?? {}) } }
81
+ return {
82
+ ...row,
83
+ ...patch,
84
+ dimensions: { ...(patch.dimensions ?? row.dimensions ?? {}) },
85
+ }
86
+ })
87
+ emitRows(next)
88
+ }
89
+
90
+ function addRow() {
91
+ if (!canEdit.value) return
92
+ emitRows([
93
+ ...rows.value.map((row) => ({ ...row, dimensions: { ...(row.dimensions ?? {}) } })),
94
+ createGeneralJournalEntryLine(),
95
+ ])
96
+ }
97
+
98
+ function removeRow(rowIndex: number) {
99
+ if (!canEdit.value) return
100
+ const next = rows.value
101
+ .filter((_, index) => index !== rowIndex)
102
+ .map((row) => ({ ...row, dimensions: { ...(row.dimensions ?? {}) } }))
103
+ emitRows(next.length > 0 ? next : [createGeneralJournalEntryLine()])
104
+ }
105
+
106
+ function humanizeDimensionLabel(code: string): string {
107
+ const raw = String(code ?? '').trim()
108
+ if (!raw) return 'Dimension'
109
+ const last = raw.includes('.') ? raw.split('.').pop() ?? raw : raw
110
+ return last
111
+ .replace(/_/g, ' ')
112
+ .replace(/\b\w/g, (m) => m.toUpperCase())
113
+ }
114
+
115
+ function cellKey(rowKey: string, dimensionId: string): string {
116
+ return `${rowKey}:${dimensionId}`
117
+ }
118
+
119
+ watch(
120
+ () => props.preloadedAccountContexts,
121
+ (next) => {
122
+ accountContextCache.value = { ...(next ?? {}) }
123
+ },
124
+ { immediate: true, deep: true },
125
+ )
126
+
127
+ function hasCachedAccountContext(accountId: string): boolean {
128
+ return Object.prototype.hasOwnProperty.call(accountContextCache.value, accountId)
129
+ }
130
+
131
+ function rowByClientKey(clientKey: string): GeneralJournalEntryEditorLineModel | undefined {
132
+ return rows.value.find((entry) => entry.clientKey === clientKey)
133
+ }
134
+
135
+ function clearLoadingContext(rowKey: string) {
136
+ if (!loadingContexts.value[rowKey]) return
137
+ const next = { ...loadingContexts.value }
138
+ delete next[rowKey]
139
+ loadingContexts.value = next
140
+ }
141
+
142
+ async function ensureAccountContext(row: GeneralJournalEntryEditorLineModel) {
143
+ const accountId = row.account?.id
144
+ if (!accountId) {
145
+ clearLoadingContext(row.clientKey)
146
+ accountContextsByRow.value = { ...accountContextsByRow.value, [row.clientKey]: null }
147
+ return
148
+ }
149
+
150
+ const existing = accountContextsByRow.value[row.clientKey]
151
+ if (existing?.accountId === accountId) return
152
+
153
+ if (hasCachedAccountContext(accountId)) {
154
+ accountContextsByRow.value = {
155
+ ...accountContextsByRow.value,
156
+ [row.clientKey]: accountContextCache.value[accountId] ?? null,
157
+ }
158
+ return
159
+ }
160
+
161
+ const requestKey = `${row.clientKey}:${accountId}`
162
+ if (loadingContexts.value[row.clientKey] === requestKey) return
163
+
164
+ loadingContexts.value = { ...loadingContexts.value, [row.clientKey]: requestKey }
165
+ try {
166
+ const context = await getGeneralJournalEntryAccountContext(accountId)
167
+ if (loadingContexts.value[row.clientKey] !== requestKey) return
168
+
169
+ const latestRow = rowByClientKey(row.clientKey)
170
+ if (latestRow?.account?.id !== accountId) return
171
+
172
+ accountContextCache.value = { ...accountContextCache.value, [accountId]: context }
173
+ accountContextsByRow.value = { ...accountContextsByRow.value, [row.clientKey]: context }
174
+ } catch {
175
+ if (loadingContexts.value[row.clientKey] !== requestKey) return
176
+ accountContextsByRow.value = { ...accountContextsByRow.value, [row.clientKey]: null }
177
+ } finally {
178
+ if (loadingContexts.value[row.clientKey] === requestKey) clearLoadingContext(row.clientKey)
179
+ }
180
+ }
181
+
182
+ watch(
183
+ () => rows.value.map((row) => `${row.clientKey}:${row.account?.id ?? ''}`).join('|'),
184
+ async () => {
185
+ for (const row of rows.value) {
186
+ await ensureAccountContext(row)
187
+ }
188
+ },
189
+ { immediate: true },
190
+ )
191
+
192
+ function contextForRow(row: GeneralJournalEntryEditorLineModel): GeneralJournalEntryAccountContextDto | null {
193
+ return accountContextsByRow.value[row.clientKey] ?? null
194
+ }
195
+
196
+ function selectedDimensionItem(row: GeneralJournalEntryEditorLineModel, rule: GeneralJournalEntryDimensionRuleDto) {
197
+ return row.dimensions?.[rule.dimensionId] ?? null
198
+ }
199
+
200
+ async function onAccountQuery(row: GeneralJournalEntryEditorLineModel, query: string) {
201
+ const q = String(query ?? '').trim()
202
+ if (!q) {
203
+ accountItemsByRow.value = { ...accountItemsByRow.value, [row.clientKey]: [] }
204
+ return
205
+ }
206
+
207
+ const items = await lookupStore.searchCoa(q)
208
+ accountItemsByRow.value = { ...accountItemsByRow.value, [row.clientKey]: items }
209
+ }
210
+
211
+ function onAccountSelect(rowIndex: number, item: GeneralJournalEntryEditorLineModel['account']) {
212
+ const key = rows.value[rowIndex]?.clientKey
213
+ if (key) clearLoadingContext(key)
214
+
215
+ updateRow(rowIndex, { account: item, dimensions: {} })
216
+
217
+ if (!key) return
218
+
219
+ if (!item) {
220
+ accountContextsByRow.value = { ...accountContextsByRow.value, [key]: null }
221
+ return
222
+ }
223
+
224
+ if (hasCachedAccountContext(item.id)) {
225
+ accountContextsByRow.value = {
226
+ ...accountContextsByRow.value,
227
+ [key]: accountContextCache.value[item.id] ?? null,
228
+ }
229
+ return
230
+ }
231
+
232
+ accountContextsByRow.value = { ...accountContextsByRow.value, [key]: null }
233
+ }
234
+
235
+ async function onDimensionQuery(
236
+ row: GeneralJournalEntryEditorLineModel,
237
+ rule: GeneralJournalEntryDimensionRuleDto,
238
+ query: string,
239
+ ) {
240
+ const q = String(query ?? '').trim()
241
+ const lookup = rule.lookup
242
+ const key = cellKey(row.clientKey, rule.dimensionId)
243
+
244
+ if (!q || !lookup) {
245
+ dimensionItemsByCell.value = { ...dimensionItemsByCell.value, [key]: [] }
246
+ return
247
+ }
248
+
249
+ let items: GeneralJournalEntryEditorLineModel['account'][] = []
250
+ if (lookup.kind === 'catalog') items = await lookupStore.searchCatalog(lookup.catalogType, q)
251
+ else if (lookup.kind === 'coa') items = await lookupStore.searchCoa(q)
252
+ else if (lookup.kind === 'document') items = await lookupStore.searchDocuments(lookup.documentTypes, q)
253
+
254
+ dimensionItemsByCell.value = { ...dimensionItemsByCell.value, [key]: items }
255
+ }
256
+
257
+ function onDimensionSelect(
258
+ rowIndex: number,
259
+ rule: GeneralJournalEntryDimensionRuleDto,
260
+ item: GeneralJournalEntryEditorLineModel['account'],
261
+ ) {
262
+ const current = rows.value[rowIndex]
263
+ if (!current) return
264
+
265
+ const nextDimensions = { ...(current.dimensions ?? {}) }
266
+ if (!item) delete nextDimensions[rule.dimensionId]
267
+ else nextDimensions[rule.dimensionId] = item
268
+
269
+ updateRow(rowIndex, { dimensions: nextDimensions })
270
+ }
271
+
272
+ function dimensionItems(row: GeneralJournalEntryEditorLineModel, rule: GeneralJournalEntryDimensionRuleDto) {
273
+ return dimensionItemsByCell.value[cellKey(row.clientKey, rule.dimensionId)] ?? []
274
+ }
275
+
276
+ async function openAccount(row: GeneralJournalEntryEditorLineModel) {
277
+ const target = await buildLookupFieldTargetUrl({
278
+ hint: { kind: 'coa' },
279
+ value: row.account,
280
+ route,
281
+ })
282
+
283
+ if (!target) return
284
+ await router.push(target)
285
+ }
286
+
287
+ async function openDimension(row: GeneralJournalEntryEditorLineModel, rule: GeneralJournalEntryDimensionRuleDto) {
288
+ const target = await buildLookupFieldTargetUrl({
289
+ hint: rule.lookup ?? null,
290
+ value: selectedDimensionItem(row, rule),
291
+ route,
292
+ })
293
+
294
+ if (!target) return
295
+ await router.push(target)
296
+ }
297
+
298
+ function badgeToneForDiff(): 'success' | 'warn' {
299
+ return Math.abs(totals.value.diff) < 0.000001 ? 'success' : 'warn'
300
+ }
301
+ </script>
302
+
303
+ <template>
304
+ <div class="overflow-hidden rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card">
305
+ <div class="border-b border-ngb-border px-4 py-3">
306
+ <div class="text-sm font-semibold text-ngb-text">Lines</div>
307
+ <div class="mt-0.5 text-xs text-ngb-muted">Debit/credit lines with dimension-aware lookups resolved from the selected account.</div>
308
+
309
+ <div class="mt-3 flex flex-wrap items-center gap-2">
310
+ <NgbBadge tone="neutral">Debit: {{ formatGeneralJournalEntryMoney(totals.debit) }}</NgbBadge>
311
+ <NgbBadge tone="neutral">Credit: {{ formatGeneralJournalEntryMoney(totals.credit) }}</NgbBadge>
312
+ <NgbBadge :tone="badgeToneForDiff()">Difference: {{ formatGeneralJournalEntryMoney(totals.diff) }}</NgbBadge>
313
+ </div>
314
+ </div>
315
+
316
+ <table class="w-full table-fixed text-sm">
317
+ <colgroup>
318
+ <col style="width:44px" />
319
+ <col style="width:140px" />
320
+ <col />
321
+ <col style="width:160px" />
322
+ <col style="width:320px" />
323
+ <col style="width:40px" />
324
+ </colgroup>
325
+
326
+ <thead class="bg-ngb-bg text-xs text-ngb-muted">
327
+ <tr>
328
+ <th class="border-r border-dotted border-ngb-border px-2 py-2 text-right font-semibold">#</th>
329
+ <th class="border-r border-dotted border-ngb-border px-3 py-2 font-semibold truncate">Side</th>
330
+ <th class="border-r border-dotted border-ngb-border px-3 py-2 font-semibold truncate">Account</th>
331
+ <th class="border-r border-dotted border-ngb-border px-3 py-2 font-semibold truncate">Amount</th>
332
+ <th class="border-r border-dotted border-ngb-border px-3 py-2 font-semibold truncate">Memo</th>
333
+ <th class="px-2 py-2" />
334
+ </tr>
335
+ </thead>
336
+
337
+ <tbody>
338
+ <template v-for="(row, rowIndex) in rows" :key="row.clientKey">
339
+ <tr class="border-t border-ngb-border align-top transition-colors hover:bg-ngb-bg">
340
+ <td class="border-r border-dotted border-ngb-border px-2 py-1 align-top text-right text-ngb-muted">
341
+ <div class="flex h-8 items-center justify-end">{{ rowIndex + 1 }}</div>
342
+ </td>
343
+
344
+ <td class="border-r border-dotted border-ngb-border px-0 py-1 align-top">
345
+ <NgbSelect
346
+ :model-value="row.side"
347
+ :options="sideOptions"
348
+ :disabled="!canEdit"
349
+ variant="grid"
350
+ @update:model-value="updateRow(rowIndex, { side: Number($event ?? 1) })"
351
+ />
352
+ </td>
353
+
354
+ <td class="border-r border-dotted border-ngb-border px-0 py-1 align-top">
355
+ <NgbLookup
356
+ :model-value="row.account"
357
+ :items="accountItemsByRow[row.clientKey] ?? []"
358
+ :readonly="!canEdit"
359
+ :show-open="!!row.account"
360
+ :show-clear="canEdit && !!row.account"
361
+ variant="grid"
362
+ placeholder="Type account code or name…"
363
+ @query="onAccountQuery(row, $event)"
364
+ @update:model-value="onAccountSelect(rowIndex, $event)"
365
+ @open="void openAccount(row)"
366
+ />
367
+ </td>
368
+
369
+ <td class="border-r border-dotted border-ngb-border px-1 py-1 align-top">
370
+ <NgbInput
371
+ :model-value="row.amount"
372
+ type="number"
373
+ :disabled="!canEdit"
374
+ variant="grid"
375
+ placeholder="0.00"
376
+ @update:model-value="updateRow(rowIndex, { amount: String($event ?? '') })"
377
+ />
378
+ </td>
379
+
380
+ <td class="border-r border-dotted border-ngb-border px-1 py-1 align-top">
381
+ <NgbInput
382
+ :model-value="row.memo"
383
+ :disabled="!canEdit"
384
+ variant="grid"
385
+ placeholder="Memo"
386
+ @update:model-value="updateRow(rowIndex, { memo: String($event ?? '') })"
387
+ />
388
+ </td>
389
+
390
+ <td class="px-1 py-1 align-middle">
391
+ <button
392
+ type="button"
393
+ class="flex h-8 w-8 items-center justify-center rounded-[var(--ngb-radius)] text-ngb-muted hover:bg-ngb-bg hover:text-ngb-text ngb-focus"
394
+ title="Delete"
395
+ :disabled="!canEdit"
396
+ @click="removeRow(rowIndex)"
397
+ >
398
+ <NgbIcon name="trash" :size="16" />
399
+ </button>
400
+ </td>
401
+ </tr>
402
+
403
+ <tr v-if="row.account && loadingContexts[row.clientKey]" class="border-t border-ngb-border bg-ngb-bg/40">
404
+ <td colspan="6" class="px-4 py-3 text-sm text-ngb-muted">Loading dimension rules…</td>
405
+ </tr>
406
+
407
+ <tr v-if="contextForRow(row)?.dimensionRules?.length" class="border-t border-ngb-border bg-ngb-bg/40">
408
+ <td colspan="6" class="px-4 py-3">
409
+ <div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
410
+ <div
411
+ v-for="rule in contextForRow(row)?.dimensionRules ?? []"
412
+ :key="rule.dimensionId"
413
+ class="rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-3"
414
+ >
415
+ <div class="mb-2 flex items-center gap-2">
416
+ <div class="text-xs font-semibold text-ngb-text">{{ humanizeDimensionLabel(rule.dimensionCode) }}</div>
417
+ <NgbBadge v-if="rule.isRequired" tone="warn">Required</NgbBadge>
418
+ </div>
419
+
420
+ <NgbLookup
421
+ :model-value="selectedDimensionItem(row, rule)"
422
+ :items="dimensionItems(row, rule)"
423
+ :disabled="!rule.lookup"
424
+ :readonly="!canEdit"
425
+ :show-open="!!rule.lookup && !!selectedDimensionItem(row, rule)"
426
+ :show-clear="canEdit && !!rule.lookup && !!selectedDimensionItem(row, rule)"
427
+ placeholder="Type to search…"
428
+ @query="onDimensionQuery(row, rule, $event)"
429
+ @update:model-value="onDimensionSelect(rowIndex, rule, $event)"
430
+ @open="void openDimension(row, rule)"
431
+ />
432
+ </div>
433
+ </div>
434
+ </td>
435
+ </tr>
436
+ </template>
437
+ </tbody>
438
+ </table>
439
+
440
+ <div class="flex items-center justify-between gap-3 border-t border-ngb-border px-3 py-2">
441
+ <div class="text-sm text-ngb-muted">{{ rows.length }} line(s)</div>
442
+
443
+ <button
444
+ type="button"
445
+ class="inline-flex h-9 items-center gap-2 rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card px-3 text-sm text-ngb-text hover:bg-ngb-bg ngb-focus"
446
+ :disabled="!canEdit"
447
+ @click="addRow"
448
+ >
449
+ <NgbIcon name="plus" :size="16" />
450
+ <span>Add line</span>
451
+ </button>
452
+ </div>
453
+ </div>
454
+ </template>
@@ -0,0 +1,186 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+
5
+ import NgbRegisterGrid from '../components/register/NgbRegisterGrid.vue'
6
+ import NgbEntityListPageHeader from '../metadata/NgbEntityListPageHeader.vue'
7
+ import NgbDocumentPeriodFilter from '../metadata/NgbDocumentPeriodFilter.vue'
8
+ import NgbRecycleBinFilter from '../metadata/NgbRecycleBinFilter.vue'
9
+ import {
10
+ monthValueToDateOnlyEnd,
11
+ monthValueToDateOnlyStart,
12
+ useMonthPagedListQuery,
13
+ } from '../metadata/monthPagedListQuery'
14
+ import { navigateBack } from '../router/backNavigation'
15
+ import { normalizeDocumentStatusValue } from '../editor/documentStatus'
16
+ import { toErrorMessage } from '../utils/errorMessage'
17
+ import {
18
+ generalJournalEntryApprovalStateLabel,
19
+ generalJournalEntryJournalTypeLabel,
20
+ generalJournalEntrySourceLabel,
21
+ } from './generalJournalEntry'
22
+ import { getGeneralJournalEntryPage } from './generalJournalEntryApi'
23
+ import { buildGeneralJournalEntriesPath } from './navigation'
24
+ import type { GeneralJournalEntryPageDto } from './generalJournalEntryTypes'
25
+
26
+ const props = withDefaults(defineProps<{
27
+ title?: string
28
+ backTarget?: string | null
29
+ storageKey?: string | null
30
+ }>(), {
31
+ title: 'Journal Entries',
32
+ backTarget: '/',
33
+ storageKey: null,
34
+ })
35
+
36
+ const route = useRoute()
37
+ const router = useRouter()
38
+
39
+ const loading = ref(false)
40
+ const error = ref<string | null>(null)
41
+ const page = ref<GeneralJournalEntryPageDto | null>(null)
42
+ let loadSequence = 0
43
+
44
+ const {
45
+ offset,
46
+ limit,
47
+ trashMode,
48
+ periodFromMonth,
49
+ periodToMonth,
50
+ nextPage,
51
+ prevPage,
52
+ } = useMonthPagedListQuery({
53
+ route,
54
+ router,
55
+ })
56
+
57
+ const resolvedBackTarget = computed(() => String(props.backTarget ?? '').trim() || '/')
58
+ const resolvedStorageKey = computed(() => String(props.storageKey ?? '').trim() || `ngb:accounting:gje:list:${route.path}`)
59
+
60
+ function fmtDate(value: string | null | undefined): string {
61
+ const s = String(value ?? '').trim()
62
+ if (!s) return '—'
63
+ const d = new Date(s)
64
+ if (Number.isNaN(d.getTime())) return s
65
+ return d.toLocaleDateString()
66
+ }
67
+
68
+ async function load() {
69
+ const seq = ++loadSequence
70
+ loading.value = true
71
+ error.value = null
72
+ try {
73
+ const nextPage = await getGeneralJournalEntryPage({
74
+ offset: offset.value,
75
+ limit: limit.value,
76
+ dateFrom: monthValueToDateOnlyStart(periodFromMonth.value) ?? null,
77
+ dateTo: monthValueToDateOnlyEnd(periodToMonth.value) ?? null,
78
+ trash: trashMode.value,
79
+ })
80
+ if (seq !== loadSequence) return
81
+ page.value = nextPage
82
+ } catch (cause) {
83
+ if (seq !== loadSequence) return
84
+ error.value = toErrorMessage(cause, 'Failed to load journal entries.')
85
+ } finally {
86
+ if (seq === loadSequence) loading.value = false
87
+ }
88
+ }
89
+
90
+ watch(
91
+ () => [route.fullPath],
92
+ () => {
93
+ void load()
94
+ },
95
+ { immediate: true },
96
+ )
97
+
98
+ const columns = [
99
+ { key: 'display', title: 'Display', width: 320, pinned: 'left' as const },
100
+ { key: 'dateUtc', title: 'Date', width: 120, format: (value: unknown) => fmtDate(value as string | null | undefined) },
101
+ { key: 'journalType', title: 'Journal Type', width: 140, format: (value: unknown) => generalJournalEntryJournalTypeLabel(value) },
102
+ { key: 'approvalState', title: 'Approval', width: 130, format: (value: unknown) => generalJournalEntryApprovalStateLabel(value) },
103
+ { key: 'source', title: 'Source', width: 100, format: (value: unknown) => generalJournalEntrySourceLabel(value) },
104
+ { key: 'memo', title: 'Memo', width: 260 },
105
+ ]
106
+
107
+ const rows = computed(() => {
108
+ return (page.value?.items ?? []).map((item) => {
109
+ const status = normalizeDocumentStatusValue(item.documentStatus)
110
+ const isMarkedForDeletion = item.isMarkedForDeletion || status === 3
111
+
112
+ return {
113
+ key: item.id,
114
+ __status: status === 2 ? 'posted' : isMarkedForDeletion ? 'marked' : 'saved',
115
+ status,
116
+ isMarkedForDeletion,
117
+ display: item.display ?? item.number ?? item.id,
118
+ dateUtc: item.dateUtc,
119
+ journalType: item.journalType,
120
+ approvalState: item.approvalState,
121
+ source: item.source,
122
+ memo: item.memo ?? '—',
123
+ }
124
+ })
125
+ })
126
+
127
+ function openCreate() {
128
+ void router.push(buildGeneralJournalEntriesPath(null, { basePath: route.path }))
129
+ }
130
+
131
+ function openEdit(id: string) {
132
+ void router.push(buildGeneralJournalEntriesPath(id, { basePath: route.path }))
133
+ }
134
+ </script>
135
+
136
+ <template>
137
+ <div data-testid="journal-entry-list-page" class="flex h-full min-h-0 flex-col">
138
+ <NgbEntityListPageHeader
139
+ :title="title"
140
+ :can-back="route.path !== resolvedBackTarget"
141
+ :items-count="page ? page.items.length : null"
142
+ :total="page?.total ?? null"
143
+ :loading="loading"
144
+ :show-filter="false"
145
+ :disable-prev="offset === 0"
146
+ :disable-next="(page?.items?.length ?? 0) < limit"
147
+ @back="navigateBack(router, route, resolvedBackTarget)"
148
+ @refresh="load"
149
+ @create="openCreate"
150
+ @prev="prevPage"
151
+ @next="nextPage"
152
+ >
153
+ <template #filters>
154
+ <NgbDocumentPeriodFilter
155
+ :from-month="periodFromMonth"
156
+ :to-month="periodToMonth"
157
+ :disabled="loading"
158
+ @update:fromMonth="periodFromMonth = $event"
159
+ @update:toMonth="periodToMonth = $event"
160
+ />
161
+
162
+ <NgbRecycleBinFilter v-model="trashMode" :disabled="loading" />
163
+ </template>
164
+ </NgbEntityListPageHeader>
165
+
166
+ <div class="flex flex-1 min-h-0 flex-col gap-4 bg-ngb-bg p-6">
167
+ <div v-if="error" class="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">
168
+ {{ error }}
169
+ </div>
170
+
171
+ <NgbRegisterGrid
172
+ class="flex-1 min-h-0"
173
+ fill-height
174
+ :show-panel="false"
175
+ :columns="columns"
176
+ :rows="rows"
177
+ :show-totals="false"
178
+ :storage-key="resolvedStorageKey"
179
+ :activate-on-row-click="true"
180
+ @rowActivate="openEdit(String($event))"
181
+ />
182
+
183
+ <div v-if="loading" class="text-sm text-ngb-muted">Loading…</div>
184
+ </div>
185
+ </div>
186
+ </template>