@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,324 @@
1
+ import { getPeriodClosingCalendar } from '../accounting/periodClosingApi'
2
+ import type { PeriodClosingCalendarDto } from '../accounting/periodClosingTypes'
3
+ import { isReferenceValue } from '../metadata/entityModel'
4
+ import { ReportRowKind, type ReportCellDto, type ReportExecutionResponseDto, type ReportSheetRowDto } from '../reporting/types'
5
+ import { normalizeDateOnlyValue } from '../utils/dateValues'
6
+ import { toErrorMessage } from '../utils/errorMessage'
7
+
8
+ const DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/
9
+ const MONTH_KEY_RE = /^(\d{4})-(\d{2})$/
10
+ const DEFAULT_PAGE_LIMIT = 200
11
+ const MAX_PAGED_REQUESTS = 20
12
+
13
+ export type DashboardCaptureResult<T> = {
14
+ value: T | null
15
+ warning: string | null
16
+ }
17
+
18
+ export type DashboardMonthWindow = {
19
+ labels: string[]
20
+ monthKeys: string[]
21
+ pointDates: Date[]
22
+ }
23
+
24
+ export type DashboardPeriodClosingSummary = {
25
+ pendingCloseCount: number
26
+ lastClosedPeriod: string | null
27
+ nextClosablePeriod: string | null
28
+ firstGapPeriod: string | null
29
+ }
30
+
31
+ export type DashboardDocumentLike = {
32
+ id: string
33
+ status?: unknown
34
+ display?: string | null
35
+ payload?: {
36
+ fields?: Record<string, unknown> | null
37
+ } | null
38
+ }
39
+
40
+ export type DashboardPageResponse<TItem> = {
41
+ items?: TItem[] | null
42
+ total?: number | null
43
+ }
44
+
45
+ export type DashboardDocumentPageLoader<TDocument> = (
46
+ documentType: string,
47
+ options: {
48
+ offset: number
49
+ limit: number
50
+ filters?: Record<string, string>
51
+ },
52
+ ) => Promise<DashboardPageResponse<TDocument>>
53
+
54
+ export async function captureDashboardValue<T>(
55
+ label: string,
56
+ factory: () => Promise<T>,
57
+ ): Promise<DashboardCaptureResult<T>> {
58
+ try {
59
+ return { value: await factory(), warning: null }
60
+ } catch (error) {
61
+ return { value: null, warning: `${label}: ${toErrorMessage(error, 'Request failed.')}` }
62
+ }
63
+ }
64
+
65
+ export function parseDashboardUtcDateOnly(value: string | null | undefined): Date | null {
66
+ const normalized = normalizeDateOnlyValue(value)
67
+ if (!normalized) return null
68
+
69
+ const match = DATE_ONLY_RE.exec(normalized)
70
+ if (!match) return null
71
+
72
+ const year = Number(match[1])
73
+ const month = Number(match[2])
74
+ const day = Number(match[3])
75
+ const date = new Date(Date.UTC(year, month - 1, day))
76
+ return Number.isNaN(date.getTime()) ? null : date
77
+ }
78
+
79
+ export function toDashboardUtcDateOnly(date: Date): string {
80
+ const year = date.getUTCFullYear()
81
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0')
82
+ const day = String(date.getUTCDate()).padStart(2, '0')
83
+ return `${year}-${month}-${day}`
84
+ }
85
+
86
+ export function startOfDashboardUtcMonth(date: Date): Date {
87
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1))
88
+ }
89
+
90
+ export function endOfDashboardUtcMonth(date: Date): Date {
91
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0))
92
+ }
93
+
94
+ export function addDashboardUtcMonths(date: Date, offset: number): Date {
95
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + offset, date.getUTCDate()))
96
+ }
97
+
98
+ export function addDashboardUtcDays(date: Date, offset: number): Date {
99
+ return new Date(date.getTime() + offset * 24 * 60 * 60 * 1000)
100
+ }
101
+
102
+ export function toDashboardUtcMonthKey(date: Date): string {
103
+ return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`
104
+ }
105
+
106
+ export function formatDashboardMonthLabel(monthKey: string): string {
107
+ const match = MONTH_KEY_RE.exec(String(monthKey ?? '').trim())
108
+ if (!match) return monthKey
109
+
110
+ const year = Number(match[1])
111
+ const month = Number(match[2])
112
+ return new Date(Date.UTC(year, month - 1, 1)).toLocaleString(undefined, {
113
+ month: 'short',
114
+ year: 'numeric',
115
+ timeZone: 'UTC',
116
+ })
117
+ }
118
+
119
+ export function formatDashboardMonthChip(dateOnly: string | null | undefined): string | null {
120
+ const date = parseDashboardUtcDateOnly(dateOnly)
121
+ if (!date) return null
122
+
123
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1)).toLocaleString(undefined, {
124
+ month: 'short',
125
+ year: 'numeric',
126
+ timeZone: 'UTC',
127
+ })
128
+ }
129
+
130
+ export function compareDashboardUtcDateOnly(left: string | null | undefined, right: string | null | undefined): number {
131
+ const leftDate = parseDashboardUtcDateOnly(left)
132
+ const rightDate = parseDashboardUtcDateOnly(right)
133
+ if (!leftDate && !rightDate) return 0
134
+ if (!leftDate) return 1
135
+ if (!rightDate) return -1
136
+ return leftDate.getTime() - rightDate.getTime()
137
+ }
138
+
139
+ export function isDashboardUtcDateWithinRange(
140
+ dateOnly: string | null | undefined,
141
+ fromInclusive: Date,
142
+ toInclusive: Date,
143
+ ): boolean {
144
+ const value = parseDashboardUtcDateOnly(dateOnly)
145
+ if (!value) return false
146
+ return value.getTime() >= fromInclusive.getTime() && value.getTime() <= toInclusive.getTime()
147
+ }
148
+
149
+ export function formatDashboardMoney(value: number): string {
150
+ const normalized = Number.isFinite(value) ? value : 0
151
+ return normalized.toLocaleString(undefined, {
152
+ style: 'currency',
153
+ currency: 'USD',
154
+ minimumFractionDigits: 0,
155
+ maximumFractionDigits: 0,
156
+ })
157
+ }
158
+
159
+ export function formatDashboardMoneyCompact(value: number): string {
160
+ const normalized = Number.isFinite(value) ? value : 0
161
+ const abs = Math.abs(normalized)
162
+ if (abs >= 1_000_000) return `${normalized < 0 ? '-' : ''}$${(abs / 1_000_000).toFixed(1)}M`
163
+ if (abs >= 1_000) return `${normalized < 0 ? '-' : ''}$${(abs / 1_000).toFixed(1)}K`
164
+ return formatDashboardMoney(normalized)
165
+ }
166
+
167
+ export function formatDashboardPercent(value: number, digits = 1): string {
168
+ if (!Number.isFinite(value)) return '0%'
169
+ return `${value.toFixed(digits)}%`
170
+ }
171
+
172
+ export function formatDashboardCount(value: number): string {
173
+ const normalized = Number.isFinite(value) ? value : 0
174
+ return normalized.toLocaleString()
175
+ }
176
+
177
+ export function toDashboardMoney(value: unknown): number {
178
+ const numeric = Number(value ?? 0)
179
+ return Number.isFinite(numeric) ? numeric : 0
180
+ }
181
+
182
+ export function toDashboardInteger(value: unknown): number {
183
+ const numeric = Number(value ?? 0)
184
+ return Number.isFinite(numeric) ? Math.round(numeric) : 0
185
+ }
186
+
187
+ export function dashboardFieldValue(document: DashboardDocumentLike, key: string): unknown {
188
+ return (document.payload?.fields ?? {})[key]
189
+ }
190
+
191
+ export function dashboardFieldMoney(document: DashboardDocumentLike, key: string): number {
192
+ return toDashboardMoney(dashboardFieldValue(document, key))
193
+ }
194
+
195
+ export function dashboardFieldDateOnly(document: DashboardDocumentLike, key: string): string | null {
196
+ return normalizeDateOnlyValue(dashboardFieldValue(document, key))
197
+ }
198
+
199
+ export function dashboardFieldDisplay(document: DashboardDocumentLike, key: string): string | null {
200
+ const value = dashboardFieldValue(document, key)
201
+ if (isReferenceValue(value)) return value.display
202
+
203
+ const raw = String(value ?? '').trim()
204
+ return raw.length > 0 ? raw : null
205
+ }
206
+
207
+ export function isPostedDashboardDocument(document: DashboardDocumentLike, postedStatus = 2): boolean {
208
+ return Number(document.status) === postedStatus
209
+ }
210
+
211
+ export async function fetchAllPagedDashboardDocuments<TDocument>(
212
+ loadPage: DashboardDocumentPageLoader<TDocument>,
213
+ documentType: string,
214
+ opts?: {
215
+ periodFrom?: string | null
216
+ periodTo?: string | null
217
+ filters?: Record<string, string>
218
+ limit?: number
219
+ maxPages?: number
220
+ },
221
+ ): Promise<TDocument[]> {
222
+ const limit = opts?.limit ?? DEFAULT_PAGE_LIMIT
223
+ const maxPages = opts?.maxPages ?? MAX_PAGED_REQUESTS
224
+ const filters: Record<string, string> = {
225
+ deleted: 'active',
226
+ ...(opts?.filters ?? {}),
227
+ }
228
+
229
+ if (opts?.periodFrom) filters.periodFrom = opts.periodFrom
230
+ if (opts?.periodTo) filters.periodTo = opts.periodTo
231
+
232
+ const items: TDocument[] = []
233
+ let offset = 0
234
+ let pageCount = 0
235
+ let total: number | null = null
236
+
237
+ while (pageCount < maxPages) {
238
+ const page = await loadPage(documentType, { offset, limit, filters })
239
+ items.push(...(page.items ?? []))
240
+ total = typeof page.total === 'number' ? page.total : total
241
+ pageCount += 1
242
+
243
+ const loaded = items.length
244
+ const reachedTotal = total != null ? loaded >= total : (page.items?.length ?? 0) < limit
245
+ if (reachedTotal || (page.items?.length ?? 0) === 0) break
246
+ offset += limit
247
+ }
248
+
249
+ return items
250
+ }
251
+
252
+ export function isDashboardReportRowKind(row: ReportSheetRowDto, kind: ReportRowKind): boolean {
253
+ return row.rowKind === kind
254
+ || String(row.rowKind).toLowerCase() === String(ReportRowKind[kind]).toLowerCase()
255
+ }
256
+
257
+ export function dashboardReportColumnIndexMap(response: ReportExecutionResponseDto): Map<string, number> {
258
+ return new Map((response.sheet.columns ?? []).map((column, index) => [String(column.code ?? '').trim(), index]))
259
+ }
260
+
261
+ export function dashboardReportCellByCode(
262
+ row: ReportSheetRowDto,
263
+ columns: Map<string, number>,
264
+ code: string,
265
+ ): ReportCellDto | null {
266
+ const index = columns.get(code)
267
+ if (index == null) return null
268
+ return row.cells[index] ?? null
269
+ }
270
+
271
+ export function dashboardReportCellDisplay(
272
+ row: ReportSheetRowDto,
273
+ columns: Map<string, number>,
274
+ code: string,
275
+ ): string {
276
+ return String(dashboardReportCellByCode(row, columns, code)?.display ?? '').trim()
277
+ }
278
+
279
+ export function dashboardReportCellNumber(
280
+ row: ReportSheetRowDto,
281
+ columns: Map<string, number>,
282
+ code: string,
283
+ ): number {
284
+ const cell = dashboardReportCellByCode(row, columns, code)
285
+ if (!cell) return 0
286
+ return toDashboardMoney(cell.value ?? cell.display)
287
+ }
288
+
289
+ export function buildDashboardMonthWindow(asOfDate: Date, count: number): DashboardMonthWindow {
290
+ const monthKeys: string[] = []
291
+ const labels: string[] = []
292
+ const pointDates: Date[] = []
293
+
294
+ for (let index = count - 1; index >= 0; index -= 1) {
295
+ const monthDate = startOfDashboardUtcMonth(addDashboardUtcMonths(asOfDate, -index))
296
+ const monthKey = toDashboardUtcMonthKey(monthDate)
297
+ monthKeys.push(monthKey)
298
+ labels.push(new Date(Date.UTC(monthDate.getUTCFullYear(), monthDate.getUTCMonth(), 1)).toLocaleString(undefined, {
299
+ month: 'short',
300
+ timeZone: 'UTC',
301
+ }))
302
+
303
+ const currentMonthKey = toDashboardUtcMonthKey(asOfDate)
304
+ pointDates.push(monthKey === currentMonthKey ? asOfDate : endOfDashboardUtcMonth(monthDate))
305
+ }
306
+
307
+ return { labels, monthKeys, pointDates }
308
+ }
309
+
310
+ export async function loadDashboardPeriodClosingSummary(
311
+ asOf: string,
312
+ loadCalendar: (year: number) => Promise<PeriodClosingCalendarDto> = getPeriodClosingCalendar,
313
+ ): Promise<DashboardPeriodClosingSummary> {
314
+ const asOfDate = parseDashboardUtcDateOnly(asOf)
315
+ if (!asOfDate) throw new Error('Invalid as-of date.')
316
+
317
+ const calendar = await loadCalendar(asOfDate.getUTCFullYear())
318
+ return {
319
+ pendingCloseCount: (calendar.months ?? []).filter((month) => month.hasActivity && !month.isClosed).length,
320
+ lastClosedPeriod: calendar.latestContiguousClosedPeriod ?? calendar.latestClosedPeriod ?? null,
321
+ nextClosablePeriod: calendar.nextClosablePeriod ?? null,
322
+ firstGapPeriod: calendar.firstGapPeriod ?? null,
323
+ }
324
+ }
@@ -0,0 +1,49 @@
1
+ import { defineStore } from 'pinia'
2
+ import { httpGet } from '../api/http'
3
+ import { toErrorMessage } from '../utils/errorMessage'
4
+
5
+ export type MainMenuItem = {
6
+ kind: string
7
+ code: string
8
+ label: string
9
+ route: string
10
+ icon?: string
11
+ ordinal: number
12
+ }
13
+
14
+ export type MainMenuGroup = {
15
+ label: string
16
+ items: MainMenuItem[]
17
+ ordinal: number
18
+ icon?: string | null
19
+ }
20
+
21
+ export type MainMenuDto = {
22
+ groups: MainMenuGroup[]
23
+ }
24
+
25
+ export const useMainMenuStore = defineStore('mainMenu', {
26
+ state: () => ({
27
+ groups: [] as MainMenuGroup[],
28
+ isLoading: false,
29
+ error: null as string | null,
30
+ }),
31
+ actions: {
32
+ async load() {
33
+ this.isLoading = true
34
+ this.error = null
35
+
36
+ try {
37
+ const dto = await httpGet<MainMenuDto>('/api/main-menu')
38
+ this.groups = dto?.groups ?? []
39
+ } catch (cause) {
40
+ this.groups = []
41
+ this.error = toErrorMessage(cause, 'Failed to load main menu')
42
+ // eslint-disable-next-line no-console
43
+ console.error(cause)
44
+ } finally {
45
+ this.isLoading = false
46
+ }
47
+ },
48
+ },
49
+ })
@@ -0,0 +1,31 @@
1
+ export type SiteNavNode = {
2
+ id: string;
3
+ label: string;
4
+ route?: string;
5
+ icon?: string | null;
6
+ badge?: string;
7
+ disabled?: boolean;
8
+ children?: SiteNavNode[];
9
+ };
10
+
11
+ export type SiteQuickLink = {
12
+ id: string;
13
+ title: string;
14
+ subtitle?: string;
15
+ route: string;
16
+ };
17
+
18
+ // Settings hub (opened via topbar gear icon).
19
+ // This is intentionally simple and app-defined (the host app decides which pages are "settings").
20
+ export type SiteSettingsItem = {
21
+ label: string;
22
+ description?: string;
23
+ route: string;
24
+ // Optional icon name from NgbIcon (string to keep this type decoupled from the icon implementation).
25
+ icon?: string;
26
+ };
27
+
28
+ export type SiteSettingsSection = {
29
+ label: string;
30
+ items: SiteSettingsItem[];
31
+ };
@@ -0,0 +1,98 @@
1
+ import { computed, ref, watch } from 'vue'
2
+ import { useRoute, useRouter, type RouteLocationNormalizedLoaded, type Router } from 'vue-router'
3
+
4
+ import { normalizeDateOnlyQueryValue, replaceCleanRouteQuery } from '../router/queryParams'
5
+ import { toDateOnlyValue } from '../utils/dateValues'
6
+ import { toErrorMessage } from '../utils/errorMessage'
7
+
8
+ export type DashboardWarningResolver<TDashboard> = (
9
+ dashboard: TDashboard | null,
10
+ ) => readonly string[] | null | undefined
11
+
12
+ export type UseDashboardPageStateArgs<TDashboard> = {
13
+ load: (asOf: string) => Promise<TDashboard>
14
+ queryKey?: string
15
+ route?: RouteLocationNormalizedLoaded
16
+ router?: Router
17
+ fallbackAsOf?: () => string
18
+ resolveWarnings?: DashboardWarningResolver<TDashboard>
19
+ }
20
+
21
+ function defaultResolveWarnings<TDashboard>(
22
+ dashboard: TDashboard | null,
23
+ ): readonly string[] | null | undefined {
24
+ if (!dashboard || typeof dashboard !== 'object' || !('warnings' in dashboard)) return null
25
+ const value = (dashboard as { warnings?: unknown }).warnings
26
+ return Array.isArray(value) ? value.map((item) => String(item ?? '').trim()) : null
27
+ }
28
+
29
+ export function useDashboardPageState<TDashboard>(
30
+ args: UseDashboardPageStateArgs<TDashboard>,
31
+ ) {
32
+ const route = args.route ?? useRoute()
33
+ const router = args.router ?? useRouter()
34
+ const queryKey = String(args.queryKey ?? 'asOf').trim() || 'asOf'
35
+ const resolveAsOf = args.fallbackAsOf ?? (() => toDateOnlyValue(new Date()))
36
+ const resolveWarnings = args.resolveWarnings ?? defaultResolveWarnings
37
+
38
+ const dashboard = ref<TDashboard | null>(null)
39
+ const loading = ref(false)
40
+ const error = ref<string | null>(null)
41
+ const refreshTick = ref(0)
42
+
43
+ let loadSequence = 0
44
+
45
+ const asOf = computed<string>({
46
+ get: () => normalizeDateOnlyQueryValue(route.query[queryKey]) ?? resolveAsOf(),
47
+ set: (value) => {
48
+ void replaceCleanRouteQuery(route, router, { [queryKey]: value })
49
+ },
50
+ })
51
+
52
+ const warnings = computed(() => {
53
+ const values = resolveWarnings(dashboard.value) ?? []
54
+ return values
55
+ .map((value) => String(value ?? '').trim())
56
+ .filter((value, index, items) => value.length > 0 && items.indexOf(value) === index)
57
+ })
58
+
59
+ async function reload(): Promise<void> {
60
+ const seq = ++loadSequence
61
+ loading.value = true
62
+ error.value = null
63
+
64
+ try {
65
+ const next = await args.load(asOf.value)
66
+ if (seq !== loadSequence) return
67
+ dashboard.value = next
68
+ } catch (err: unknown) {
69
+ if (seq !== loadSequence) return
70
+ dashboard.value = null
71
+ error.value = toErrorMessage(err, 'Request failed.')
72
+ } finally {
73
+ if (seq === loadSequence) loading.value = false
74
+ }
75
+ }
76
+
77
+ function refresh(): void {
78
+ refreshTick.value += 1
79
+ }
80
+
81
+ watch(
82
+ () => [asOf.value, refreshTick.value] as const,
83
+ () => {
84
+ void reload()
85
+ },
86
+ { immediate: true },
87
+ )
88
+
89
+ return {
90
+ asOf,
91
+ dashboard,
92
+ error,
93
+ loading,
94
+ refresh,
95
+ reload,
96
+ warnings,
97
+ }
98
+ }
@@ -0,0 +1,95 @@
1
+ import { computed, onMounted, ref, watch } from 'vue'
2
+
3
+ import { readCookie, readStorageString, writeCookie, writeStorageString } from '../utils/storage'
4
+
5
+ export type ThemeMode = 'light' | 'dark' | 'system'
6
+ const KEY = 'ngb.theme'
7
+ const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 400
8
+
9
+ function prefersDark() {
10
+ return typeof window !== 'undefined'
11
+ && window.matchMedia
12
+ && window.matchMedia('(prefers-color-scheme: dark)').matches
13
+ }
14
+
15
+ function readThemeCookie(): ThemeMode | null {
16
+ const value = readCookie(KEY)
17
+ return value === 'light' || value === 'dark' || value === 'system'
18
+ ? value
19
+ : null
20
+ }
21
+
22
+ function isIpAddress(hostname: string) {
23
+ return /^(?:\d{1,3}\.){3}\d{1,3}$/.test(hostname)
24
+ || hostname.includes(':')
25
+ }
26
+
27
+ function resolveSharedCookieDomain(hostname: string) {
28
+ const normalized = hostname.trim().toLowerCase()
29
+
30
+ if (!normalized || normalized === 'localhost' || isIpAddress(normalized) || !normalized.includes('.'))
31
+ return null
32
+
33
+ return normalized
34
+ }
35
+
36
+ function persistThemeCookie(mode: ThemeMode) {
37
+ if (typeof document === 'undefined' || typeof window === 'undefined')
38
+ return
39
+
40
+ const secure = window.location.protocol === 'https:'
41
+ writeCookie(KEY, mode, {
42
+ path: '/',
43
+ maxAgeSeconds: COOKIE_MAX_AGE_SECONDS,
44
+ sameSite: 'Lax',
45
+ secure,
46
+ })
47
+
48
+ const sharedDomain = resolveSharedCookieDomain(window.location.hostname)
49
+ if (sharedDomain)
50
+ writeCookie(KEY, mode, {
51
+ path: '/',
52
+ maxAgeSeconds: COOKIE_MAX_AGE_SECONDS,
53
+ sameSite: 'Lax',
54
+ secure,
55
+ domain: sharedDomain,
56
+ })
57
+ }
58
+
59
+ export function useTheme() {
60
+ const mode = ref<ThemeMode>('system')
61
+
62
+ onMounted(() => {
63
+ const saved = readStorageString('local', KEY)
64
+ if (saved === 'light' || saved === 'dark' || saved === 'system') {
65
+ mode.value = saved
66
+ return
67
+ }
68
+
69
+ const cookie = readThemeCookie()
70
+ if (cookie) mode.value = cookie
71
+ })
72
+
73
+ const resolved = computed<'light' | 'dark'>(() => {
74
+ if (mode.value === 'system') return prefersDark() ? 'dark' : 'light'
75
+ return mode.value
76
+ })
77
+
78
+ function apply() {
79
+ const el = document.documentElement
80
+ if (resolved.value === 'dark') el.classList.add('dark')
81
+ else el.classList.remove('dark')
82
+ }
83
+
84
+ watch([mode, resolved], () => {
85
+ void writeStorageString('local', KEY, mode.value)
86
+ persistThemeCookie(mode.value)
87
+ apply()
88
+ }, { immediate: true })
89
+
90
+ function toggle() {
91
+ mode.value = resolved.value === 'dark' ? 'light' : 'dark'
92
+ }
93
+
94
+ return { mode, resolved, toggle }
95
+ }
@@ -0,0 +1,43 @@
1
+ type PlainObject = Record<string, unknown>
2
+
3
+ function isPlainObject(value: unknown): value is PlainObject {
4
+ if (value == null || typeof value !== 'object') return false
5
+ const prototype = Object.getPrototypeOf(value)
6
+ return prototype === Object.prototype || prototype === null
7
+ }
8
+
9
+ function clonePlainDataFallback<T>(value: T): T {
10
+ if (value == null || typeof value !== 'object') return value
11
+
12
+ if (value instanceof Date) {
13
+ return new Date(value.getTime()) as T
14
+ }
15
+
16
+ if (Array.isArray(value)) {
17
+ return value.map((item) => clonePlainDataFallback(item)) as T
18
+ }
19
+
20
+ if (isPlainObject(value)) {
21
+ const out: PlainObject = {}
22
+ for (const [key, entry] of Object.entries(value)) {
23
+ out[key] = clonePlainDataFallback(entry)
24
+ }
25
+ return out as T
26
+ }
27
+
28
+ return value
29
+ }
30
+
31
+ export function clonePlainData<T>(value: T): T {
32
+ if (value == null || typeof value !== 'object') return value
33
+
34
+ if (typeof structuredClone === 'function') {
35
+ try {
36
+ return structuredClone(value)
37
+ } catch {
38
+ // Fall back to recursive cloning for plain DTO-like data.
39
+ }
40
+ }
41
+
42
+ return clonePlainDataFallback(value)
43
+ }