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