@open-mercato/core 0.6.7-develop.6751.1.ac823a3d26 → 0.6.7-develop.6758.1.697eade236
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/attachments/api/route.js +16 -2
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/lib/upload-limits.js +33 -0
- package/dist/modules/attachments/lib/upload-limits.js.map +2 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js +52 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
- package/dist/modules/currencies/di.js +4 -0
- package/dist/modules/currencies/di.js.map +2 -2
- package/dist/modules/currencies/services/baseCurrencyService.js +42 -0
- package/dist/modules/currencies/services/baseCurrencyService.js.map +7 -0
- package/dist/modules/customers/analytics.js +3 -1
- package/dist/modules/customers/analytics.js.map +2 -2
- package/dist/modules/customers/api/deals/aggregate/route.js +5 -4
- package/dist/modules/customers/api/deals/aggregate/route.js.map +2 -2
- package/dist/modules/customers/api/deals/summary/route.js +5 -4
- package/dist/modules/customers/api/deals/summary/route.js.map +2 -2
- package/dist/modules/customers/lib/optionalBaseCurrency.js +19 -0
- package/dist/modules/customers/lib/optionalBaseCurrency.js.map +7 -0
- package/dist/modules/dashboards/api/widgets/data/batch/route.js +8 -1
- package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/route.js +8 -1
- package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/schema.js +2 -1
- package/dist/modules/dashboards/api/widgets/data/schema.js.map +2 -2
- package/dist/modules/dashboards/components/UnlabelledAmountNotice.js +33 -0
- package/dist/modules/dashboards/components/UnlabelledAmountNotice.js.map +7 -0
- package/dist/modules/dashboards/lib/aggregations.js +17 -0
- package/dist/modules/dashboards/lib/aggregations.js.map +2 -2
- package/dist/modules/dashboards/lib/formatters.js +84 -11
- package/dist/modules/dashboards/lib/formatters.js.map +2 -2
- package/dist/modules/dashboards/lib/optionalBaseCurrency.js +11 -0
- package/dist/modules/dashboards/lib/optionalBaseCurrency.js.map +7 -0
- package/dist/modules/dashboards/services/widgetDataService.js +86 -6
- package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
- package/dist/modules/dashboards/subscribers/invalidateWidgetDataCache.js +26 -0
- package/dist/modules/dashboards/subscribers/invalidateWidgetDataCache.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +9 -3
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +10 -4
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +9 -3
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +8 -3
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +26 -18
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +22 -14
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +10 -4
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +2 -2
- package/dist/modules/notifications/api/route.js +62 -3
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/sales/analytics.js +7 -3
- package/dist/modules/sales/analytics.js.map +2 -2
- package/dist/modules/sales/api/document-history/route.js +2 -1
- package/dist/modules/sales/api/document-history/route.js.map +2 -2
- package/dist/modules/sales/lib/historyHelpers.js +69 -1
- package/dist/modules/sales/lib/historyHelpers.js.map +2 -2
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js +51 -2
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/api/route.ts +16 -2
- package/src/modules/attachments/lib/upload-limits.ts +35 -0
- package/src/modules/communication_channels/api/post/test-seed/route.ts +69 -2
- package/src/modules/currencies/di.ts +4 -1
- package/src/modules/currencies/services/baseCurrencyService.ts +59 -0
- package/src/modules/customers/analytics.ts +2 -0
- package/src/modules/customers/api/deals/aggregate/route.ts +5 -4
- package/src/modules/customers/api/deals/summary/route.ts +5 -4
- package/src/modules/customers/lib/optionalBaseCurrency.ts +34 -0
- package/src/modules/dashboards/api/widgets/data/batch/route.ts +8 -1
- package/src/modules/dashboards/api/widgets/data/route.ts +8 -1
- package/src/modules/dashboards/api/widgets/data/schema.ts +1 -0
- package/src/modules/dashboards/components/UnlabelledAmountNotice.tsx +43 -0
- package/src/modules/dashboards/i18n/de.json +2 -0
- package/src/modules/dashboards/i18n/en.json +2 -0
- package/src/modules/dashboards/i18n/es.json +2 -0
- package/src/modules/dashboards/i18n/pl.json +2 -0
- package/src/modules/dashboards/lib/aggregations.ts +35 -0
- package/src/modules/dashboards/lib/formatters.ts +137 -12
- package/src/modules/dashboards/lib/optionalBaseCurrency.ts +22 -0
- package/src/modules/dashboards/services/widgetDataService.ts +110 -4
- package/src/modules/dashboards/subscribers/invalidateWidgetDataCache.ts +32 -0
- package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +9 -3
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +9 -3
- package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +9 -3
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +7 -2
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +25 -15
- package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +21 -11
- package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +9 -3
- package/src/modules/notifications/api/route.ts +88 -3
- package/src/modules/sales/analytics.ts +4 -0
- package/src/modules/sales/api/document-history/route.ts +1 -0
- package/src/modules/sales/i18n/de.json +19 -0
- package/src/modules/sales/i18n/en.json +19 -0
- package/src/modules/sales/i18n/es.json +19 -0
- package/src/modules/sales/i18n/pl.json +19 -0
- package/src/modules/sales/lib/historyHelpers.ts +88 -1
- package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +64 -3
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
5
5
|
import { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'
|
|
6
|
-
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'
|
|
8
8
|
import { DateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'
|
|
9
9
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
10
10
|
import { DEFAULT_SETTINGS, hydrateSettings, type SalesByRegionSettings } from './config'
|
|
11
11
|
import type { WidgetDataResponse } from '../../../services/widgetDataService'
|
|
12
|
-
import {
|
|
12
|
+
import { createCurrencyFormatters } from '../../../lib/formatters'
|
|
13
|
+
import { UnlabelledAmountNotice } from '../../../components/UnlabelledAmountNotice'
|
|
13
14
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
14
15
|
|
|
15
16
|
const logger = createLogger('dashboards').child({ component: 'sales-by-region' })
|
|
@@ -42,10 +43,13 @@ const SalesByRegionWidget: React.FC<DashboardWidgetComponentProps<SalesByRegionS
|
|
|
42
43
|
onRefreshStateChange,
|
|
43
44
|
}) => {
|
|
44
45
|
const t = useT()
|
|
46
|
+
const locale = useLocale()
|
|
45
47
|
const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])
|
|
46
48
|
const [data, setData] = React.useState<BarChartDataItem[]>([])
|
|
49
|
+
const [currency, setCurrency] = React.useState<string | null>(null)
|
|
47
50
|
const [loading, setLoading] = React.useState(true)
|
|
48
51
|
const [error, setError] = React.useState<string | null>(null)
|
|
52
|
+
const money = React.useMemo(() => createCurrencyFormatters(currency, '--', locale), [currency, locale])
|
|
49
53
|
|
|
50
54
|
const fetchWidgetData = useWidgetData()
|
|
51
55
|
const refresh = React.useCallback(async () => {
|
|
@@ -59,6 +63,7 @@ const SalesByRegionWidget: React.FC<DashboardWidgetComponentProps<SalesByRegionS
|
|
|
59
63
|
Revenue: item.value ?? 0,
|
|
60
64
|
}))
|
|
61
65
|
setData(chartData)
|
|
66
|
+
setCurrency(result.metadata?.currency ?? null)
|
|
62
67
|
} catch (err) {
|
|
63
68
|
logger.error('Failed to load sales by region data', { err })
|
|
64
69
|
setError(t('dashboards.analytics.widgets.salesByRegion.error', 'Failed to load data'))
|
|
@@ -106,19 +111,24 @@ const SalesByRegionWidget: React.FC<DashboardWidgetComponentProps<SalesByRegionS
|
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
return (
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
114
|
+
<div className="flex flex-col h-full">
|
|
115
|
+
<div className="flex-1 min-h-0">
|
|
116
|
+
<BarChart
|
|
117
|
+
data={data}
|
|
118
|
+
index="region"
|
|
119
|
+
categories={['Revenue']}
|
|
120
|
+
categoryLabels={{ Revenue: t('dashboards.analytics.widgets.topCustomers.column.revenue', 'Revenue') }}
|
|
121
|
+
loading={loading}
|
|
122
|
+
error={error}
|
|
123
|
+
layout="horizontal"
|
|
124
|
+
valueFormatter={money.formatCompact}
|
|
125
|
+
colors={['cyan']}
|
|
126
|
+
showLegend={false}
|
|
127
|
+
emptyMessage={t('dashboards.analytics.widgets.salesByRegion.empty', 'No regional sales data for this period')}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
<UnlabelledAmountNotice currency={currency} loading={loading} error={error} />
|
|
131
|
+
</div>
|
|
122
132
|
)
|
|
123
133
|
}
|
|
124
134
|
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
5
5
|
import { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'
|
|
6
|
-
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { TopNTable, type TopNTableColumn } from '@open-mercato/ui/backend/charts'
|
|
8
8
|
import { DateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'
|
|
9
9
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
10
10
|
import { DEFAULT_SETTINGS, hydrateSettings, type TopCustomersSettings } from './config'
|
|
11
11
|
import type { WidgetDataResponse } from '../../../services/widgetDataService'
|
|
12
|
-
import {
|
|
12
|
+
import { createCurrencyFormatters } from '../../../lib/formatters'
|
|
13
|
+
import { UnlabelledAmountNotice } from '../../../components/UnlabelledAmountNotice'
|
|
13
14
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
14
15
|
|
|
15
16
|
const logger = createLogger('dashboards').child({ component: 'top-customers' })
|
|
@@ -54,10 +55,13 @@ const TopCustomersWidget: React.FC<DashboardWidgetComponentProps<TopCustomersSet
|
|
|
54
55
|
onRefreshStateChange,
|
|
55
56
|
}) => {
|
|
56
57
|
const t = useT()
|
|
58
|
+
const locale = useLocale()
|
|
57
59
|
const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])
|
|
58
60
|
const [data, setData] = React.useState<CustomerRow[]>([])
|
|
61
|
+
const [currency, setCurrency] = React.useState<string | null>(null)
|
|
59
62
|
const [loading, setLoading] = React.useState(true)
|
|
60
63
|
const [error, setError] = React.useState<string | null>(null)
|
|
64
|
+
const money = React.useMemo(() => createCurrencyFormatters(currency, '--', locale), [currency, locale])
|
|
61
65
|
|
|
62
66
|
const unknownLabel = t('dashboards.analytics.labels.unknown', 'Unknown')
|
|
63
67
|
const columns: TopNTableColumn<CustomerRow>[] = React.useMemo(
|
|
@@ -76,10 +80,10 @@ const TopCustomersWidget: React.FC<DashboardWidgetComponentProps<TopCustomersSet
|
|
|
76
80
|
key: 'revenue',
|
|
77
81
|
header: t('dashboards.analytics.widgets.topCustomers.column.revenue', 'Revenue'),
|
|
78
82
|
align: 'right',
|
|
79
|
-
formatter: (value: unknown) =>
|
|
83
|
+
formatter: (value: unknown) => money.formatSafe(value),
|
|
80
84
|
},
|
|
81
85
|
],
|
|
82
|
-
[t, unknownLabel],
|
|
86
|
+
[t, unknownLabel, money],
|
|
83
87
|
)
|
|
84
88
|
|
|
85
89
|
const fetchWidgetData = useWidgetData()
|
|
@@ -95,6 +99,7 @@ const TopCustomersWidget: React.FC<DashboardWidgetComponentProps<TopCustomersSet
|
|
|
95
99
|
revenue: item.value ?? 0,
|
|
96
100
|
}))
|
|
97
101
|
setData(tableData)
|
|
102
|
+
setCurrency(result.metadata?.currency ?? null)
|
|
98
103
|
} catch (err) {
|
|
99
104
|
logger.error('Failed to load top customers data', { err })
|
|
100
105
|
setError(t('dashboards.analytics.widgets.topCustomers.error', 'Failed to load data'))
|
|
@@ -142,13 +147,18 @@ const TopCustomersWidget: React.FC<DashboardWidgetComponentProps<TopCustomersSet
|
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
return (
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
<div className="flex flex-col h-full">
|
|
151
|
+
<div className="flex-1 min-h-0">
|
|
152
|
+
<TopNTable
|
|
153
|
+
data={data}
|
|
154
|
+
columns={columns}
|
|
155
|
+
loading={loading}
|
|
156
|
+
error={error}
|
|
157
|
+
emptyMessage={t('dashboards.analytics.widgets.topCustomers.empty', 'No customer data for this period')}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
<UnlabelledAmountNotice currency={currency} loading={loading} error={error} />
|
|
161
|
+
</div>
|
|
152
162
|
)
|
|
153
163
|
}
|
|
154
164
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
5
5
|
import { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'
|
|
6
|
-
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'
|
|
8
8
|
import { DateRangeSelect, InlineDateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'
|
|
9
9
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
} from '@open-mercato/ui/primitives/select'
|
|
17
17
|
import { DEFAULT_SETTINGS, hydrateSettings, type TopProductsSettings } from './config'
|
|
18
18
|
import type { WidgetDataResponse } from '../../../services/widgetDataService'
|
|
19
|
-
import {
|
|
19
|
+
import { createCurrencyFormatters } from '../../../lib/formatters'
|
|
20
|
+
import { UnlabelledAmountNotice } from '../../../components/UnlabelledAmountNotice'
|
|
20
21
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
21
22
|
|
|
22
23
|
const logger = createLogger('dashboards').child({ component: 'top-products' })
|
|
@@ -68,11 +69,14 @@ const TopProductsWidget: React.FC<DashboardWidgetComponentProps<TopProductsSetti
|
|
|
68
69
|
onRefreshStateChange,
|
|
69
70
|
}) => {
|
|
70
71
|
const t = useT()
|
|
72
|
+
const locale = useLocale()
|
|
71
73
|
const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])
|
|
72
74
|
const [data, setData] = React.useState<BarChartDataItem[]>([])
|
|
75
|
+
const [currency, setCurrency] = React.useState<string | null>(null)
|
|
73
76
|
const [loading, setLoading] = React.useState(true)
|
|
74
77
|
const [error, setError] = React.useState<string | null>(null)
|
|
75
78
|
const fetchingRef = React.useRef(false)
|
|
79
|
+
const money = React.useMemo(() => createCurrencyFormatters(currency, '--', locale), [currency, locale])
|
|
76
80
|
|
|
77
81
|
const fetchWidgetData = useWidgetData()
|
|
78
82
|
const refresh = React.useCallback(async () => {
|
|
@@ -88,6 +92,7 @@ const TopProductsWidget: React.FC<DashboardWidgetComponentProps<TopProductsSetti
|
|
|
88
92
|
Revenue: item.value ?? 0,
|
|
89
93
|
}))
|
|
90
94
|
setData(chartData)
|
|
95
|
+
setCurrency(result.metadata?.currency ?? null)
|
|
91
96
|
} catch (err) {
|
|
92
97
|
logger.error('Failed to load top products data', { err })
|
|
93
98
|
setError(t('dashboards.analytics.widgets.topProducts.error', 'Failed to load data'))
|
|
@@ -172,12 +177,13 @@ const TopProductsWidget: React.FC<DashboardWidgetComponentProps<TopProductsSetti
|
|
|
172
177
|
loading={loading}
|
|
173
178
|
error={error}
|
|
174
179
|
layout={hydrated.layout}
|
|
175
|
-
valueFormatter={
|
|
180
|
+
valueFormatter={money.formatCompact}
|
|
176
181
|
colors={['emerald']}
|
|
177
182
|
showLegend={false}
|
|
178
183
|
emptyMessage={t('dashboards.analytics.widgets.topProducts.empty', 'No product sales data for this period')}
|
|
179
184
|
/>
|
|
180
185
|
</div>
|
|
186
|
+
<UnlabelledAmountNotice currency={currency} loading={loading} error={error} />
|
|
181
187
|
</div>
|
|
182
188
|
)
|
|
183
189
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import type { EntityManager } from '@mikro-orm/core'
|
|
3
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
4
|
+
import {
|
|
5
|
+
buildCollectionTags,
|
|
6
|
+
debugCrudCache,
|
|
7
|
+
isCrudCacheEnabled,
|
|
8
|
+
resolveCrudCache,
|
|
9
|
+
} from '@open-mercato/shared/lib/crud/cache'
|
|
3
10
|
import { Notification } from '../data/entities'
|
|
4
11
|
import { listNotificationsSchema, createNotificationSchema } from '../data/validators'
|
|
5
12
|
import { toNotificationDto } from '../lib/notificationMapper'
|
|
@@ -22,6 +29,49 @@ export const metadata = {
|
|
|
22
29
|
POST: { requireAuth: true, requireFeatures: ['notifications.create'] },
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
const NOTIFICATIONS_LIST_TTL_MS = 10_000
|
|
33
|
+
const NOTIFICATIONS_LIST_CACHE_VERSION = 1
|
|
34
|
+
const NOTIFICATIONS_LIST_RESOURCE = 'notifications.notification'
|
|
35
|
+
|
|
36
|
+
type NotificationsListPayload = {
|
|
37
|
+
items: ReturnType<typeof toNotificationDto>[]
|
|
38
|
+
total: number
|
|
39
|
+
page: number
|
|
40
|
+
pageSize: number
|
|
41
|
+
totalPages: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildNotificationsListCacheKey(
|
|
45
|
+
userId: string,
|
|
46
|
+
input: z.infer<typeof listNotificationsSchema>,
|
|
47
|
+
): string {
|
|
48
|
+
const filterSignature = JSON.stringify({
|
|
49
|
+
status: Array.isArray(input.status)
|
|
50
|
+
? [...input.status].sort((left, right) => left.localeCompare(right))
|
|
51
|
+
: input.status ?? null,
|
|
52
|
+
type: input.type ?? null,
|
|
53
|
+
severity: input.severity ?? null,
|
|
54
|
+
sourceEntityType: input.sourceEntityType ?? null,
|
|
55
|
+
sourceEntityId: input.sourceEntityId ?? null,
|
|
56
|
+
since: input.since ?? null,
|
|
57
|
+
page: input.page,
|
|
58
|
+
pageSize: input.pageSize,
|
|
59
|
+
})
|
|
60
|
+
return `notifications:list:v${NOTIFICATIONS_LIST_CACHE_VERSION}:u=${userId}:filters=${filterSignature}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isNotificationsListPayload(value: unknown): value is NotificationsListPayload {
|
|
64
|
+
if (!value || typeof value !== 'object') return false
|
|
65
|
+
const payload = value as Partial<NotificationsListPayload>
|
|
66
|
+
return (
|
|
67
|
+
Array.isArray(payload.items)
|
|
68
|
+
&& typeof payload.total === 'number'
|
|
69
|
+
&& typeof payload.page === 'number'
|
|
70
|
+
&& typeof payload.pageSize === 'number'
|
|
71
|
+
&& typeof payload.totalPages === 'number'
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
25
75
|
export async function GET(req: Request) {
|
|
26
76
|
const { ctx, scope } = await resolveNotificationContext(req)
|
|
27
77
|
const em = ctx.container.resolve('em') as EntityManager
|
|
@@ -29,9 +79,27 @@ export async function GET(req: Request) {
|
|
|
29
79
|
const url = new URL(req.url)
|
|
30
80
|
const queryParams = Object.fromEntries(url.searchParams.entries())
|
|
31
81
|
const input = listNotificationsSchema.parse(queryParams)
|
|
82
|
+
const userId = scope.userId
|
|
83
|
+
const cache = userId && isCrudCacheEnabled()
|
|
84
|
+
? resolveCrudCache(ctx.container)
|
|
85
|
+
: null
|
|
86
|
+
const cacheKey = cache && userId ? buildNotificationsListCacheKey(userId, input) : null
|
|
87
|
+
|
|
88
|
+
if (cache && cacheKey) {
|
|
89
|
+
try {
|
|
90
|
+
const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))
|
|
91
|
+
if (isNotificationsListPayload(cached)) {
|
|
92
|
+
return Response.json(cached)
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
debugCrudCache('notifications-list-cache-read-failed', {
|
|
96
|
+
error: error instanceof Error ? error.message : String(error),
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
32
100
|
|
|
33
101
|
const filters: Record<string, unknown> = {
|
|
34
|
-
recipientUserId:
|
|
102
|
+
recipientUserId: userId,
|
|
35
103
|
tenantId: scope.tenantId,
|
|
36
104
|
...buildNotificationReadScopeWhere(scope),
|
|
37
105
|
}
|
|
@@ -68,13 +136,30 @@ export async function GET(req: Request) {
|
|
|
68
136
|
|
|
69
137
|
const items = notifications.map(toNotificationDto)
|
|
70
138
|
|
|
71
|
-
|
|
139
|
+
const payload: NotificationsListPayload = {
|
|
72
140
|
items,
|
|
73
141
|
total,
|
|
74
142
|
page: input.page,
|
|
75
143
|
pageSize: input.pageSize,
|
|
76
144
|
totalPages: Math.ceil(total / input.pageSize),
|
|
77
|
-
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (cache && cacheKey) {
|
|
148
|
+
try {
|
|
149
|
+
await runWithCacheTenant(scope.tenantId, () =>
|
|
150
|
+
cache.set(cacheKey, payload, {
|
|
151
|
+
ttl: NOTIFICATIONS_LIST_TTL_MS,
|
|
152
|
+
tags: buildCollectionTags(NOTIFICATIONS_LIST_RESOURCE, scope.tenantId, [null]),
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
} catch (error) {
|
|
156
|
+
debugCrudCache('notifications-list-cache-write-failed', {
|
|
157
|
+
error: error instanceof Error ? error.message : String(error),
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return Response.json(payload)
|
|
78
163
|
}
|
|
79
164
|
|
|
80
165
|
export async function POST(req: Request) {
|
|
@@ -9,6 +9,7 @@ export const analyticsConfig: AnalyticsModuleConfig = {
|
|
|
9
9
|
tableName: 'sales_orders',
|
|
10
10
|
dateField: 'placed_at',
|
|
11
11
|
defaultScopeFields: ['tenant_id', 'organization_id'],
|
|
12
|
+
currencyField: 'currencyCode',
|
|
12
13
|
},
|
|
13
14
|
fieldMappings: {
|
|
14
15
|
id: { dbColumn: 'id', type: 'uuid' },
|
|
@@ -40,9 +41,11 @@ export const analyticsConfig: AnalyticsModuleConfig = {
|
|
|
40
41
|
tableName: 'sales_order_lines',
|
|
41
42
|
dateField: 'created_at',
|
|
42
43
|
defaultScopeFields: ['tenant_id', 'organization_id'],
|
|
44
|
+
currencyField: 'currencyCode',
|
|
43
45
|
},
|
|
44
46
|
fieldMappings: {
|
|
45
47
|
id: { dbColumn: 'id', type: 'uuid' },
|
|
48
|
+
currencyCode: { dbColumn: 'currency_code', type: 'text' },
|
|
46
49
|
totalGrossAmount: { dbColumn: 'total_gross_amount', type: 'numeric' },
|
|
47
50
|
totalNetAmount: { dbColumn: 'total_net_amount', type: 'numeric' },
|
|
48
51
|
unitGrossPrice: { dbColumn: 'unit_gross_price', type: 'numeric' },
|
|
@@ -64,6 +67,7 @@ export const analyticsConfig: AnalyticsModuleConfig = {
|
|
|
64
67
|
tableName: 'sales_quotes',
|
|
65
68
|
dateField: 'created_at',
|
|
66
69
|
defaultScopeFields: ['tenant_id', 'organization_id'],
|
|
70
|
+
currencyField: 'currencyCode',
|
|
67
71
|
},
|
|
68
72
|
fieldMappings: {
|
|
69
73
|
id: { dbColumn: 'id', type: 'uuid' },
|
|
@@ -171,6 +171,7 @@ const historyEntrySchema = z.object({
|
|
|
171
171
|
statusTo: z.string().nullable().optional(),
|
|
172
172
|
documentKind: z.enum(['order', 'quote']).optional(),
|
|
173
173
|
commandId: z.string().optional(),
|
|
174
|
+
changedFields: z.array(z.string()).optional(),
|
|
174
175
|
}).optional(),
|
|
175
176
|
})
|
|
176
177
|
|
|
@@ -927,8 +927,27 @@
|
|
|
927
927
|
"sales.documents.form.submit": "Erstellen",
|
|
928
928
|
"sales.documents.form.success": "Verkaufsdokument erstellt.",
|
|
929
929
|
"sales.documents.form.title": "Verkaufsdokument erstellen",
|
|
930
|
+
"sales.documents.history.actor.system": "System",
|
|
931
|
+
"sales.documents.history.changedFields": "Geänderte Felder: {fields}",
|
|
930
932
|
"sales.documents.history.empty": "Noch keine Verlaufseinträge.",
|
|
931
933
|
"sales.documents.history.error": "Verlauf konnte nicht geladen werden.",
|
|
934
|
+
"sales.documents.history.fields.comment": "Kommentar",
|
|
935
|
+
"sales.documents.history.fields.configuration": "Konfiguration",
|
|
936
|
+
"sales.documents.history.fields.currency": "Währung",
|
|
937
|
+
"sales.documents.history.fields.customFields": "Benutzerdefinierte Felder",
|
|
938
|
+
"sales.documents.history.fields.description": "Beschreibung",
|
|
939
|
+
"sales.documents.history.fields.discountAmount": "Rabattbetrag",
|
|
940
|
+
"sales.documents.history.fields.discountPercent": "Rabattprozentsatz",
|
|
941
|
+
"sales.documents.history.fields.name": "Name",
|
|
942
|
+
"sales.documents.history.fields.product": "Produkt",
|
|
943
|
+
"sales.documents.history.fields.promotionCode": "Aktionscode",
|
|
944
|
+
"sales.documents.history.fields.quantity": "Menge",
|
|
945
|
+
"sales.documents.history.fields.quantityUnit": "Mengeneinheit",
|
|
946
|
+
"sales.documents.history.fields.status": "Status",
|
|
947
|
+
"sales.documents.history.fields.taxRate": "Steuerklasse",
|
|
948
|
+
"sales.documents.history.fields.unitPriceGross": "Bruttostückpreis",
|
|
949
|
+
"sales.documents.history.fields.unitPriceNet": "Nettostückpreis",
|
|
950
|
+
"sales.documents.history.fields.variant": "Variante",
|
|
932
951
|
"sales.documents.history.filter.actions": "Aktionen",
|
|
933
952
|
"sales.documents.history.filter.all": "Alle",
|
|
934
953
|
"sales.documents.history.filter.comments": "Kommentare",
|
|
@@ -927,8 +927,27 @@
|
|
|
927
927
|
"sales.documents.form.submit": "Create",
|
|
928
928
|
"sales.documents.form.success": "Sales document created.",
|
|
929
929
|
"sales.documents.form.title": "Create sales document",
|
|
930
|
+
"sales.documents.history.actor.system": "System",
|
|
931
|
+
"sales.documents.history.changedFields": "Changed fields: {fields}",
|
|
930
932
|
"sales.documents.history.empty": "No history entries yet.",
|
|
931
933
|
"sales.documents.history.error": "Failed to load history.",
|
|
934
|
+
"sales.documents.history.fields.comment": "Comment",
|
|
935
|
+
"sales.documents.history.fields.configuration": "Configuration",
|
|
936
|
+
"sales.documents.history.fields.currency": "Currency",
|
|
937
|
+
"sales.documents.history.fields.customFields": "Custom fields",
|
|
938
|
+
"sales.documents.history.fields.description": "Description",
|
|
939
|
+
"sales.documents.history.fields.discountAmount": "Discount amount",
|
|
940
|
+
"sales.documents.history.fields.discountPercent": "Discount percent",
|
|
941
|
+
"sales.documents.history.fields.name": "Name",
|
|
942
|
+
"sales.documents.history.fields.product": "Product",
|
|
943
|
+
"sales.documents.history.fields.promotionCode": "Promotion code",
|
|
944
|
+
"sales.documents.history.fields.quantity": "Quantity",
|
|
945
|
+
"sales.documents.history.fields.quantityUnit": "Quantity unit",
|
|
946
|
+
"sales.documents.history.fields.status": "Status",
|
|
947
|
+
"sales.documents.history.fields.taxRate": "Tax class",
|
|
948
|
+
"sales.documents.history.fields.unitPriceGross": "Gross unit price",
|
|
949
|
+
"sales.documents.history.fields.unitPriceNet": "Net unit price",
|
|
950
|
+
"sales.documents.history.fields.variant": "Variant",
|
|
932
951
|
"sales.documents.history.filter.actions": "Actions",
|
|
933
952
|
"sales.documents.history.filter.all": "All",
|
|
934
953
|
"sales.documents.history.filter.comments": "Comments",
|
|
@@ -927,8 +927,27 @@
|
|
|
927
927
|
"sales.documents.form.submit": "Crear",
|
|
928
928
|
"sales.documents.form.success": "Documento de ventas creado.",
|
|
929
929
|
"sales.documents.form.title": "Crear documento de ventas",
|
|
930
|
+
"sales.documents.history.actor.system": "Sistema",
|
|
931
|
+
"sales.documents.history.changedFields": "Campos modificados: {fields}",
|
|
930
932
|
"sales.documents.history.empty": "Aún no hay entradas en el historial.",
|
|
931
933
|
"sales.documents.history.error": "No se pudo cargar el historial.",
|
|
934
|
+
"sales.documents.history.fields.comment": "Comentario",
|
|
935
|
+
"sales.documents.history.fields.configuration": "Configuración",
|
|
936
|
+
"sales.documents.history.fields.currency": "Moneda",
|
|
937
|
+
"sales.documents.history.fields.customFields": "Campos personalizados",
|
|
938
|
+
"sales.documents.history.fields.description": "Descripción",
|
|
939
|
+
"sales.documents.history.fields.discountAmount": "Importe de descuento",
|
|
940
|
+
"sales.documents.history.fields.discountPercent": "Porcentaje de descuento",
|
|
941
|
+
"sales.documents.history.fields.name": "Nombre",
|
|
942
|
+
"sales.documents.history.fields.product": "Producto",
|
|
943
|
+
"sales.documents.history.fields.promotionCode": "Código promocional",
|
|
944
|
+
"sales.documents.history.fields.quantity": "Cantidad",
|
|
945
|
+
"sales.documents.history.fields.quantityUnit": "Unidad de cantidad",
|
|
946
|
+
"sales.documents.history.fields.status": "Estado",
|
|
947
|
+
"sales.documents.history.fields.taxRate": "Clase fiscal",
|
|
948
|
+
"sales.documents.history.fields.unitPriceGross": "Precio unitario bruto",
|
|
949
|
+
"sales.documents.history.fields.unitPriceNet": "Precio unitario neto",
|
|
950
|
+
"sales.documents.history.fields.variant": "Variante",
|
|
932
951
|
"sales.documents.history.filter.actions": "Acciones",
|
|
933
952
|
"sales.documents.history.filter.all": "Todos",
|
|
934
953
|
"sales.documents.history.filter.comments": "Comentarios",
|
|
@@ -927,8 +927,27 @@
|
|
|
927
927
|
"sales.documents.form.submit": "Utwórz",
|
|
928
928
|
"sales.documents.form.success": "Dokument sprzedaży utworzony.",
|
|
929
929
|
"sales.documents.form.title": "Utwórz dokument sprzedaży",
|
|
930
|
+
"sales.documents.history.actor.system": "System",
|
|
931
|
+
"sales.documents.history.changedFields": "Zmienione pola: {fields}",
|
|
930
932
|
"sales.documents.history.empty": "Brak wpisów w historii.",
|
|
931
933
|
"sales.documents.history.error": "Nie udało się załadować historii.",
|
|
934
|
+
"sales.documents.history.fields.comment": "Komentarz",
|
|
935
|
+
"sales.documents.history.fields.configuration": "Konfiguracja",
|
|
936
|
+
"sales.documents.history.fields.currency": "Waluta",
|
|
937
|
+
"sales.documents.history.fields.customFields": "Pola niestandardowe",
|
|
938
|
+
"sales.documents.history.fields.description": "Opis",
|
|
939
|
+
"sales.documents.history.fields.discountAmount": "Kwota rabatu",
|
|
940
|
+
"sales.documents.history.fields.discountPercent": "Procent rabatu",
|
|
941
|
+
"sales.documents.history.fields.name": "Nazwa",
|
|
942
|
+
"sales.documents.history.fields.product": "Produkt",
|
|
943
|
+
"sales.documents.history.fields.promotionCode": "Kod promocyjny",
|
|
944
|
+
"sales.documents.history.fields.quantity": "Ilość",
|
|
945
|
+
"sales.documents.history.fields.quantityUnit": "Jednostka ilości",
|
|
946
|
+
"sales.documents.history.fields.status": "Status",
|
|
947
|
+
"sales.documents.history.fields.taxRate": "Klasa podatkowa",
|
|
948
|
+
"sales.documents.history.fields.unitPriceGross": "Cena jednostkowa brutto",
|
|
949
|
+
"sales.documents.history.fields.unitPriceNet": "Cena jednostkowa netto",
|
|
950
|
+
"sales.documents.history.fields.variant": "Wariant",
|
|
932
951
|
"sales.documents.history.filter.actions": "Akcje",
|
|
933
952
|
"sales.documents.history.filter.all": "Wszystkie",
|
|
934
953
|
"sales.documents.history.filter.comments": "Komentarze",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'
|
|
2
|
+
import { isRecord } from '@open-mercato/shared/lib/utils'
|
|
2
3
|
import type { SalesNote } from '../data/entities'
|
|
3
4
|
|
|
4
5
|
export type HistoryEntry = {
|
|
@@ -13,9 +14,90 @@ export type HistoryEntry = {
|
|
|
13
14
|
statusTo?: string | null
|
|
14
15
|
documentKind?: 'order' | 'quote'
|
|
15
16
|
commandId?: string
|
|
17
|
+
changedFields?: string[]
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
const DOCUMENT_LINE_UPSERT_COMMANDS = new Set([
|
|
22
|
+
'sales.orders.lines.upsert',
|
|
23
|
+
'sales.quotes.lines.upsert',
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
const USER_EDITABLE_LINE_FIELDS = [
|
|
27
|
+
'productId',
|
|
28
|
+
'productVariantId',
|
|
29
|
+
'name',
|
|
30
|
+
'description',
|
|
31
|
+
'comment',
|
|
32
|
+
'quantity',
|
|
33
|
+
'quantityUnit',
|
|
34
|
+
'currencyCode',
|
|
35
|
+
'unitPriceNet',
|
|
36
|
+
'unitPriceGross',
|
|
37
|
+
'discountAmount',
|
|
38
|
+
'discountPercent',
|
|
39
|
+
'taxRate',
|
|
40
|
+
'configuration',
|
|
41
|
+
'promotionCode',
|
|
42
|
+
'customFields',
|
|
43
|
+
'statusEntryId',
|
|
44
|
+
] as const
|
|
45
|
+
|
|
46
|
+
function readSnapshotLines(snapshot: unknown): Array<Record<string, unknown>> {
|
|
47
|
+
if (!isRecord(snapshot) || !Array.isArray(snapshot.lines)) return []
|
|
48
|
+
return snapshot.lines.filter(isRecord)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function historyValuesEqual(left: unknown, right: unknown): boolean {
|
|
52
|
+
if (Object.is(left, right)) return true
|
|
53
|
+
try {
|
|
54
|
+
return JSON.stringify(left) === JSON.stringify(right)
|
|
55
|
+
} catch {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Derives a concise, user-editable field list for document line updates.
|
|
62
|
+
* Calculated totals and normalized UoM fields are intentionally excluded so
|
|
63
|
+
* the timeline describes the user's edit rather than downstream recalculation.
|
|
64
|
+
*/
|
|
65
|
+
function deriveDocumentLineChangedFields(log: ActionLog): string[] {
|
|
66
|
+
if (!DOCUMENT_LINE_UPSERT_COMMANDS.has(log.commandId)) return []
|
|
67
|
+
|
|
68
|
+
const beforeById = new Map(
|
|
69
|
+
readSnapshotLines(log.snapshotBefore)
|
|
70
|
+
.filter((line) => typeof line.id === 'string')
|
|
71
|
+
.map((line) => [line.id as string, line]),
|
|
72
|
+
)
|
|
73
|
+
const afterLines = readSnapshotLines(log.snapshotAfter)
|
|
74
|
+
|
|
75
|
+
const changedFields = new Set<string>()
|
|
76
|
+
for (const afterLine of afterLines) {
|
|
77
|
+
if (typeof afterLine.id !== 'string') continue
|
|
78
|
+
const beforeLine = beforeById.get(afterLine.id)
|
|
79
|
+
if (!beforeLine) continue
|
|
80
|
+
|
|
81
|
+
for (const field of USER_EDITABLE_LINE_FIELDS) {
|
|
82
|
+
if (!historyValuesEqual(beforeLine[field], afterLine[field])) {
|
|
83
|
+
changedFields.add(field)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return USER_EDITABLE_LINE_FIELDS.filter((field) => changedFields.has(field))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function deriveHistoryChangedFields(log: ActionLog): string[] {
|
|
92
|
+
if (DOCUMENT_LINE_UPSERT_COMMANDS.has(log.commandId)) {
|
|
93
|
+
return deriveDocumentLineChangedFields(log)
|
|
94
|
+
}
|
|
95
|
+
const persisted = Array.isArray(log.changedFields)
|
|
96
|
+
? log.changedFields.filter((field): field is string => typeof field === 'string' && field.trim().length > 0)
|
|
97
|
+
: []
|
|
98
|
+
return Array.from(new Set(persisted))
|
|
99
|
+
}
|
|
100
|
+
|
|
19
101
|
function extractStatusFromSnapshot(snapshot: unknown): string | null {
|
|
20
102
|
if (!snapshot || typeof snapshot !== 'object') return null
|
|
21
103
|
const s = snapshot as Record<string, unknown>
|
|
@@ -64,9 +146,14 @@ export function normalizeActionLogToHistoryEntry(
|
|
|
64
146
|
displayUsers?: Record<string, string>,
|
|
65
147
|
): HistoryEntry {
|
|
66
148
|
const statusChange = detectStatusChange(log)
|
|
149
|
+
const changedFields = deriveHistoryChangedFields(log)
|
|
67
150
|
let entryKind: 'status' | 'action' = 'action'
|
|
68
151
|
let action = log.actionLabel || log.commandId
|
|
69
|
-
let metadata: HistoryEntry['metadata'] = {
|
|
152
|
+
let metadata: HistoryEntry['metadata'] = {
|
|
153
|
+
documentKind: kind,
|
|
154
|
+
commandId: log.commandId,
|
|
155
|
+
...(changedFields.length > 0 ? { changedFields } : {}),
|
|
156
|
+
}
|
|
70
157
|
if (statusChange) {
|
|
71
158
|
const hasStatusValues = statusChange.statusFrom != null || statusChange.statusTo != null
|
|
72
159
|
if (hasStatusValues) {
|