@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,446 @@
1
+ import { forceRefreshAccessToken, getAccessToken } from '../auth/keycloak'
2
+ import { readAppEnv } from '../env/runtimeConfig'
3
+ import type { JsonValue, QueryParams } from './types'
4
+
5
+ export type ApiProblemDetails = {
6
+ title?: string
7
+ detail?: string
8
+ status?: number
9
+ type?: string
10
+ instance?: string
11
+ traceId?: string
12
+ [key: string]: JsonValue | undefined
13
+ }
14
+
15
+ export type ApiValidationErrors = Record<string, string[]>
16
+
17
+ export type ApiValidationIssue = {
18
+ path: string
19
+ message: string
20
+ scope: string
21
+ code?: string | null
22
+ }
23
+
24
+ export type ApiErrorEnvelope = {
25
+ code?: string | null
26
+ kind?: string | null
27
+ context?: Record<string, unknown> | null
28
+ errors?: ApiValidationErrors | null
29
+ issues?: ApiValidationIssue[] | null
30
+ }
31
+
32
+ export type HttpRequestOptions = {
33
+ signal?: AbortSignal
34
+ retryOnUnauthorized?: boolean
35
+ }
36
+
37
+ function isRecord(value: unknown): value is Record<string, unknown> {
38
+ return !!value && typeof value === 'object' && !Array.isArray(value)
39
+ }
40
+
41
+ function normalizeValidationErrors(value: unknown): ApiValidationErrors | null {
42
+ if (!isRecord(value)) return null
43
+
44
+ const result: ApiValidationErrors = {}
45
+ for (const [key, raw] of Object.entries(value)) {
46
+ const messages = Array.isArray(raw)
47
+ ? raw.map((entry) => (entry == null ? '' : String(entry).trim())).filter((entry) => entry.length > 0)
48
+ : raw == null
49
+ ? []
50
+ : [String(raw).trim()].filter((entry) => entry.length > 0)
51
+
52
+ if (messages.length > 0) result[key] = messages
53
+ }
54
+
55
+ return Object.keys(result).length > 0 ? result : null
56
+ }
57
+
58
+ function normalizeValidationPath(raw: unknown): string {
59
+ let path = typeof raw === 'string' ? raw.trim() : ''
60
+ if (!path) return '_form'
61
+ if (path === '_form') return path
62
+
63
+ if (path.startsWith('$')) path = path.slice(1)
64
+ if (path.startsWith('.')) path = path.slice(1)
65
+
66
+ const stripPrefix = (value: string, prefix: string) =>
67
+ value.toLowerCase().startsWith(prefix.toLowerCase()) ? value.slice(prefix.length) : value
68
+
69
+ path = stripPrefix(path, 'request.')
70
+ path = stripPrefix(path, 'payload.')
71
+ path = stripPrefix(path, 'fields.')
72
+
73
+ path = path.replace(/^([^\.]+)\.rows\[(\d+)\]\.(.+)$/i, '$1[$2].$3')
74
+ path = path.replace(/^([^\.]+)\.rows\[(\d+)\]$/i, '$1[$2]')
75
+ path = stripPrefix(path, 'parts.')
76
+ path = path.replace(/\.rows\[(\d+)\]/gi, '[$1]')
77
+ path = path.replace(/\.rows\[\]/gi, '[]')
78
+ path = path.replace(/^\.+|\.+$/g, '')
79
+
80
+ if (!path || path.toLowerCase() === 'payload') return '_form'
81
+ return path
82
+ }
83
+
84
+ function inferIssueScope(path: string): string {
85
+ if (!path || path === '_form') return 'form'
86
+ if (/\[(?:\d+)?\]$/.test(path)) return /\[\]$/.test(path) ? 'collection' : 'row'
87
+ return 'field'
88
+ }
89
+
90
+ function normalizeValidationIssue(value: unknown): ApiValidationIssue | null {
91
+ if (!isRecord(value)) return null
92
+
93
+ const rawMessage = typeof value.message === 'string'
94
+ ? value.message
95
+ : typeof value.Message === 'string'
96
+ ? value.Message
97
+ : ''
98
+ const message = rawMessage.trim()
99
+ if (!message) return null
100
+
101
+ const path = normalizeValidationPath(
102
+ typeof value.path === 'string'
103
+ ? value.path
104
+ : typeof value.Path === 'string'
105
+ ? value.Path
106
+ : ''
107
+ )
108
+
109
+ const rawScope = typeof value.scope === 'string'
110
+ ? value.scope.trim()
111
+ : typeof value.Scope === 'string'
112
+ ? value.Scope.trim()
113
+ : ''
114
+
115
+ const code = typeof value.code === 'string'
116
+ ? value.code
117
+ : typeof value.Code === 'string'
118
+ ? value.Code
119
+ : null
120
+
121
+ return {
122
+ path,
123
+ message,
124
+ scope: rawScope || inferIssueScope(path),
125
+ code,
126
+ }
127
+ }
128
+
129
+ function normalizeValidationIssues(value: unknown): ApiValidationIssue[] | null {
130
+ if (!Array.isArray(value)) return null
131
+ const issues = value
132
+ .map((entry) => normalizeValidationIssue(entry))
133
+ .filter((entry): entry is ApiValidationIssue => !!entry)
134
+
135
+ return issues.length > 0 ? issues : null
136
+ }
137
+
138
+ function validationIssuesFromErrors(errors: ApiValidationErrors | null | undefined): ApiValidationIssue[] | null {
139
+ if (!errors) return null
140
+
141
+ const issues: ApiValidationIssue[] = []
142
+ for (const [rawPath, messages] of Object.entries(errors)) {
143
+ const path = normalizeValidationPath(rawPath)
144
+ const scope = inferIssueScope(path)
145
+ for (const message of messages ?? []) {
146
+ const text = String(message ?? '').trim()
147
+ if (!text) continue
148
+ issues.push({ path, message: text, scope, code: null })
149
+ }
150
+ }
151
+
152
+ return issues.length > 0 ? issues : null
153
+ }
154
+
155
+ function extractErrorEnvelope(body: unknown): ApiErrorEnvelope | null {
156
+ if (!isRecord(body)) return null
157
+
158
+ const nested = isRecord(body.error) ? body.error : null
159
+ const flatErrors = normalizeValidationErrors(body.errors)
160
+ const nestedErrors = normalizeValidationErrors(nested?.errors ?? nested?.Errors)
161
+ const flatIssues = normalizeValidationIssues(body.issues)
162
+ const nestedIssues = normalizeValidationIssues(nested?.issues ?? nested?.Issues)
163
+ const context = isRecord(body.context)
164
+ ? body.context
165
+ : isRecord(nested?.context)
166
+ ? nested?.context
167
+ : isRecord(nested?.Context)
168
+ ? nested?.Context
169
+ : null
170
+
171
+ const code = typeof body.errorCode === 'string'
172
+ ? body.errorCode
173
+ : typeof nested?.code === 'string'
174
+ ? nested.code
175
+ : typeof nested?.Code === 'string'
176
+ ? nested.Code
177
+ : null
178
+
179
+ const kind = typeof body.kind === 'string'
180
+ ? body.kind
181
+ : typeof nested?.kind === 'string'
182
+ ? nested.kind
183
+ : typeof nested?.Kind === 'string'
184
+ ? nested.Kind
185
+ : null
186
+
187
+ const errors = nestedErrors ?? flatErrors
188
+
189
+ return {
190
+ code,
191
+ kind,
192
+ context,
193
+ errors,
194
+ issues: nestedIssues ?? flatIssues ?? validationIssuesFromErrors(errors),
195
+ }
196
+ }
197
+
198
+ function isGenericProblemDetail(detail: string): boolean {
199
+ const normalized = detail.trim().toLowerCase()
200
+ return normalized === 'one or more validation errors has occurred.'
201
+ }
202
+
203
+ function firstValidationMessage(errors: ApiValidationErrors | null | undefined): string | null {
204
+ if (!errors) return null
205
+ for (const messages of Object.values(errors)) {
206
+ const first = messages.find((entry) => entry.trim().length > 0)
207
+ if (first) return first
208
+ }
209
+ return null
210
+ }
211
+
212
+ function firstValidationIssueMessage(issues: ApiValidationIssue[] | null | undefined): string | null {
213
+ if (!issues) return null
214
+ const first = issues.find((issue) => issue.message.trim().length > 0)
215
+ return first?.message?.trim() || null
216
+ }
217
+
218
+ export class ApiError extends Error {
219
+ readonly status: number
220
+ readonly url: string
221
+ readonly body?: unknown
222
+ readonly problem?: ApiProblemDetails | null
223
+ readonly errorCode?: string | null
224
+ readonly kind?: string | null
225
+ readonly context?: Record<string, unknown> | null
226
+ readonly errors?: ApiValidationErrors | null
227
+ readonly issues?: ApiValidationIssue[] | null
228
+
229
+ constructor(args: { message: string; status: number; url: string; body?: unknown }) {
230
+ super(args.message)
231
+ this.name = 'ApiError'
232
+ this.status = args.status
233
+ this.url = args.url
234
+ this.body = args.body
235
+
236
+ const envelope = extractErrorEnvelope(args.body)
237
+ this.problem = isRecord(args.body) ? (args.body as ApiProblemDetails) : null
238
+ this.errorCode = envelope?.code ?? null
239
+ this.kind = envelope?.kind ?? null
240
+ this.context = envelope?.context ?? null
241
+ this.errors = envelope?.errors ?? null
242
+ this.issues = envelope?.issues ?? null
243
+ }
244
+ }
245
+
246
+ function apiBaseUrl(): string {
247
+ const base = readAppEnv('VITE_API_BASE_URL')
248
+ return base.length > 0 ? base : window.location.origin
249
+ }
250
+
251
+ function isJsonContentType(contentType: string | null | undefined): boolean {
252
+ const normalized = (contentType ?? '').toLowerCase()
253
+ return normalized.includes('json')
254
+ }
255
+
256
+ function resolveUrl(url: string): string {
257
+ if (/^https?:\/\//i.test(url)) return url
258
+ const path = url.startsWith('/') ? url : `/${url}`
259
+ return new URL(path, apiBaseUrl()).toString()
260
+ }
261
+
262
+ async function tryReadJson(response: Response): Promise<unknown | null> {
263
+ if (!isJsonContentType(response.headers.get('content-type'))) return null
264
+ try {
265
+ return await response.json() as unknown
266
+ } catch {
267
+ return null
268
+ }
269
+ }
270
+
271
+ async function tryReadText(response: Response): Promise<string | null> {
272
+ try {
273
+ return await response.text()
274
+ } catch {
275
+ return null
276
+ }
277
+ }
278
+
279
+ function toApiErrorMessage(status: number, body: unknown): string {
280
+ if (!body) return `HTTP ${status}`
281
+
282
+ const envelope = extractErrorEnvelope(body)
283
+ const detail = isRecord(body) && typeof body.detail === 'string' ? body.detail.trim() : ''
284
+ const title = isRecord(body) && typeof body.title === 'string' ? body.title.trim() : ''
285
+ const firstIssue = firstValidationIssueMessage(envelope?.issues)
286
+ const firstError = firstValidationMessage(envelope?.errors)
287
+
288
+ if (detail && !isGenericProblemDetail(detail)) return detail
289
+ if (firstIssue) return firstIssue
290
+ if (firstError) return firstError
291
+
292
+ if (isRecord(body) && typeof body.message === 'string' && body.message.trim().length > 0) return body.message.trim()
293
+ if (title) return title
294
+ if (typeof envelope?.code === 'string' && envelope.code.length > 0) return `${envelope.code} (HTTP ${status})`
295
+
296
+ return `HTTP ${status}`
297
+ }
298
+
299
+ function appendQuery(url: string, query: QueryParams | null | undefined): string {
300
+ if (!query) return url
301
+ const params = new URLSearchParams()
302
+ for (const [key, value] of Object.entries(query)) {
303
+ if (value === undefined || value === null || value === '') continue
304
+ params.set(key, String(value))
305
+ }
306
+ const serialized = params.toString()
307
+ if (!serialized) return url
308
+ return url.includes('?') ? `${url}&${serialized}` : `${url}?${serialized}`
309
+ }
310
+
311
+ async function buildJsonHeaders(body: unknown, accept: string): Promise<Record<string, string>> {
312
+ const headers: Record<string, string> = { Accept: accept }
313
+ if (body != null) headers['Content-Type'] = 'application/json'
314
+
315
+ const token = await getAccessToken()
316
+ if (token) headers.Authorization = `Bearer ${token}`
317
+
318
+ return headers
319
+ }
320
+
321
+ async function buildResponse(response: Response, resolvedUrl: string): Promise<never> {
322
+ const json = await tryReadJson(response)
323
+ const message = toApiErrorMessage(response.status, json)
324
+ throw new ApiError({ message, status: response.status, url: resolvedUrl, body: json ?? undefined })
325
+ }
326
+
327
+ export async function httpRequest<T>(
328
+ method: string,
329
+ url: string,
330
+ body?: unknown,
331
+ options?: HttpRequestOptions,
332
+ ): Promise<T> {
333
+ const resolvedUrl = resolveUrl(url)
334
+ const response = await fetch(resolvedUrl, {
335
+ method,
336
+ credentials: 'omit',
337
+ headers: await buildJsonHeaders(body, 'application/json'),
338
+ body: body != null ? JSON.stringify(body) : undefined,
339
+ signal: options?.signal,
340
+ })
341
+
342
+ if (response.status === 401 && options?.retryOnUnauthorized !== false) {
343
+ const refreshedToken = await forceRefreshAccessToken().catch(() => null)
344
+ if (refreshedToken) {
345
+ return await httpRequest<T>(method, url, body, { ...options, retryOnUnauthorized: false })
346
+ }
347
+ }
348
+
349
+ if (!response.ok) await buildResponse(response, resolvedUrl)
350
+ if (response.status === 204) return undefined as unknown as T
351
+
352
+ const json = await tryReadJson(response)
353
+ if (json == null) {
354
+ const text = await tryReadText(response)
355
+ throw new ApiError({
356
+ message: `Expected JSON but got '${response.headers.get('content-type') ?? 'unknown'}' for ${resolvedUrl}`,
357
+ status: response.status,
358
+ url: resolvedUrl,
359
+ body: text ?? undefined,
360
+ })
361
+ }
362
+
363
+ return json as T
364
+ }
365
+
366
+ export async function httpGet<T>(
367
+ url: string,
368
+ query?: QueryParams | null,
369
+ options?: HttpRequestOptions,
370
+ ): Promise<T> {
371
+ return httpRequest<T>('GET', appendQuery(url, query ?? undefined), undefined, options)
372
+ }
373
+
374
+ export async function httpPost<TResponse, TBody = unknown>(
375
+ url: string,
376
+ body?: TBody,
377
+ options?: HttpRequestOptions,
378
+ ): Promise<TResponse> {
379
+ return httpRequest<TResponse>('POST', url, body, options)
380
+ }
381
+
382
+ export async function httpPut<TResponse, TBody = unknown>(
383
+ url: string,
384
+ body?: TBody,
385
+ options?: HttpRequestOptions,
386
+ ): Promise<TResponse> {
387
+ return httpRequest<TResponse>('PUT', url, body, options)
388
+ }
389
+
390
+ export async function httpDelete<TResponse, TBody = unknown>(
391
+ url: string,
392
+ body?: TBody,
393
+ options?: HttpRequestOptions,
394
+ ): Promise<TResponse> {
395
+ return httpRequest<TResponse>('DELETE', url, body, options)
396
+ }
397
+
398
+ export type HttpFileResponse = {
399
+ blob: Blob
400
+ fileName: string | null
401
+ contentType: string | null
402
+ }
403
+
404
+ function parseFileNameFromContentDisposition(value: string | null): string | null {
405
+ const raw = String(value ?? '').trim()
406
+ if (!raw) return null
407
+
408
+ const utf8Match = /filename\*=UTF-8''([^;]+)/i.exec(raw)
409
+ if (utf8Match?.[1]) {
410
+ try {
411
+ return decodeURIComponent(utf8Match[1].trim())
412
+ } catch {
413
+ return utf8Match[1].trim()
414
+ }
415
+ }
416
+
417
+ const basicMatch = /filename=\"?([^\";]+)\"?/i.exec(raw)
418
+ return basicMatch?.[1]?.trim() || null
419
+ }
420
+
421
+ export async function httpPostFile<TBody = unknown>(url: string, body?: TBody): Promise<HttpFileResponse> {
422
+ return await httpPostFileInternal(url, body, true)
423
+ }
424
+
425
+ async function httpPostFileInternal(url: string, body: unknown, retryOnUnauthorized: boolean): Promise<HttpFileResponse> {
426
+ const resolvedUrl = resolveUrl(url)
427
+ const response = await fetch(resolvedUrl, {
428
+ method: 'POST',
429
+ credentials: 'omit',
430
+ headers: await buildJsonHeaders(body, '*/*'),
431
+ body: body != null ? JSON.stringify(body) : undefined,
432
+ })
433
+
434
+ if (response.status === 401 && retryOnUnauthorized) {
435
+ const refreshedToken = await forceRefreshAccessToken().catch(() => null)
436
+ if (refreshedToken) return await httpPostFileInternal(url, body, false)
437
+ }
438
+
439
+ if (!response.ok) await buildResponse(response, resolvedUrl)
440
+
441
+ return {
442
+ blob: await response.blob(),
443
+ fileName: parseFileNameFromContentDisposition(response.headers.get('content-disposition')),
444
+ contentType: response.headers.get('content-type'),
445
+ }
446
+ }
@@ -0,0 +1,14 @@
1
+ import { httpGet, httpPost } from './http'
2
+ import type { ByIdsRequestDto, LookupItemDto } from './contracts'
3
+ import type { QueryParams } from './types'
4
+
5
+ export async function lookupCatalog(catalogType: string, q: string | null, limit = 20): Promise<LookupItemDto[]> {
6
+ const query: QueryParams = { limit }
7
+ if (q && q.trim().length > 0) query.q = q
8
+ return await httpGet<LookupItemDto[]>(`/api/catalogs/${encodeURIComponent(catalogType)}/lookup`, query)
9
+ }
10
+
11
+ export async function getCatalogLookupByIds(catalogType: string, ids: string[]): Promise<LookupItemDto[]> {
12
+ const body: ByIdsRequestDto = { ids }
13
+ return await httpPost<LookupItemDto[]>(`/api/catalogs/${encodeURIComponent(catalogType)}/by-ids`, body)
14
+ }
@@ -0,0 +1,6 @@
1
+ export type QueryParamValue = string | number | boolean
2
+ export type QueryParams = Record<string, QueryParamValue | null | undefined>
3
+ export type JsonPrimitive = string | number | boolean | null
4
+ export type JsonValue = JsonPrimitive | JsonObject | JsonValue[]
5
+ export type JsonObject = { [key: string]: JsonValue }
6
+
@@ -0,0 +1,13 @@
1
+ export {
2
+ forceRefreshAccessToken,
3
+ getAccessToken,
4
+ getAuthSnapshot,
5
+ initializeAuth,
6
+ loginWithKeycloak,
7
+ logoutFromKeycloak,
8
+ subscribeAuth,
9
+ } from './keycloak'
10
+ export type { AuthSnapshot } from './types'
11
+ export { createAuthGuard } from './router'
12
+ export type { AuthGuardStore } from './router'
13
+ export { useAuthStore } from './useAuthStore'