@open-mercato/core 0.6.4-develop.4113.1.5e87922616 → 0.6.4-develop.4133.1.48fc6c8f7b
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/auth/lib/sessionIntegrity.js +16 -13
- package/dist/modules/auth/lib/sessionIntegrity.js.map +2 -2
- package/dist/modules/customers/api/utils.js +14 -9
- package/dist/modules/customers/api/utils.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/batch/route.js +137 -0
- package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +7 -0
- package/dist/modules/dashboards/api/widgets/data/route.js +1 -75
- package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/schema.js +85 -0
- package/dist/modules/dashboards/api/widgets/data/schema.js.map +7 -0
- package/dist/modules/dashboards/lib/widgetDataBatch.js +49 -0
- package/dist/modules/dashboards/lib/widgetDataBatch.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +6 -14
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +6 -14
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +6 -14
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +6 -14
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +6 -14
- 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 +6 -14
- 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 +6 -14
- 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 +6 -14
- 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 +6 -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 +6 -14
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +2 -2
- package/dist/modules/directory/utils/organizationScope.js +33 -20
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/lib/sessionIntegrity.ts +37 -16
- package/src/modules/customers/api/utils.ts +17 -11
- package/src/modules/dashboards/api/widgets/data/batch/route.ts +168 -0
- package/src/modules/dashboards/api/widgets/data/route.ts +1 -90
- package/src/modules/dashboards/api/widgets/data/schema.ts +90 -0
- package/src/modules/dashboards/lib/widgetDataBatch.ts +89 -0
- package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +6 -16
- package/src/modules/directory/utils/organizationScope.ts +51 -20
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { z } from 'zod'
|
|
3
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
3
|
import type { CacheStrategy } from '@open-mercato/cache'
|
|
5
4
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
@@ -13,100 +12,12 @@ import {
|
|
|
13
12
|
import type { AnalyticsRegistry } from '../../../services/analyticsRegistry'
|
|
14
13
|
import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
15
14
|
import { dashboardsTag, dashboardsErrorSchema } from '../../openapi'
|
|
15
|
+
import { widgetDataRequestSchema, widgetDataResponseSchema } from './schema'
|
|
16
16
|
|
|
17
17
|
export const metadata = {
|
|
18
18
|
POST: { requireAuth: true, requireFeatures: ['analytics.view'] },
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const aggregateFunctionSchema = z.enum(['count', 'sum', 'avg', 'min', 'max'])
|
|
22
|
-
const dateGranularitySchema = z.enum(['day', 'week', 'month', 'quarter', 'year'])
|
|
23
|
-
const dateRangePresetSchema = z.enum([
|
|
24
|
-
'today',
|
|
25
|
-
'yesterday',
|
|
26
|
-
'this_week',
|
|
27
|
-
'last_week',
|
|
28
|
-
'this_month',
|
|
29
|
-
'last_month',
|
|
30
|
-
'this_quarter',
|
|
31
|
-
'last_quarter',
|
|
32
|
-
'this_year',
|
|
33
|
-
'last_year',
|
|
34
|
-
'last_7_days',
|
|
35
|
-
'last_30_days',
|
|
36
|
-
'last_90_days',
|
|
37
|
-
])
|
|
38
|
-
|
|
39
|
-
const filterOperatorSchema = z.enum([
|
|
40
|
-
'eq',
|
|
41
|
-
'neq',
|
|
42
|
-
'gt',
|
|
43
|
-
'gte',
|
|
44
|
-
'lt',
|
|
45
|
-
'lte',
|
|
46
|
-
'in',
|
|
47
|
-
'not_in',
|
|
48
|
-
'is_null',
|
|
49
|
-
'is_not_null',
|
|
50
|
-
])
|
|
51
|
-
|
|
52
|
-
const widgetDataRequestSchema = z.object({
|
|
53
|
-
entityType: z.string().min(1),
|
|
54
|
-
metric: z.object({
|
|
55
|
-
field: z.string().min(1),
|
|
56
|
-
aggregate: aggregateFunctionSchema,
|
|
57
|
-
}),
|
|
58
|
-
groupBy: z
|
|
59
|
-
.object({
|
|
60
|
-
field: z.string().min(1),
|
|
61
|
-
granularity: dateGranularitySchema.optional(),
|
|
62
|
-
limit: z.number().int().min(1).max(100).optional(),
|
|
63
|
-
resolveLabels: z.boolean().optional(),
|
|
64
|
-
})
|
|
65
|
-
.optional(),
|
|
66
|
-
filters: z
|
|
67
|
-
.array(
|
|
68
|
-
z.object({
|
|
69
|
-
field: z.string().min(1),
|
|
70
|
-
operator: filterOperatorSchema,
|
|
71
|
-
value: z.unknown().optional(),
|
|
72
|
-
}),
|
|
73
|
-
)
|
|
74
|
-
.optional(),
|
|
75
|
-
dateRange: z
|
|
76
|
-
.object({
|
|
77
|
-
field: z.string().min(1),
|
|
78
|
-
preset: dateRangePresetSchema,
|
|
79
|
-
})
|
|
80
|
-
.optional(),
|
|
81
|
-
comparison: z
|
|
82
|
-
.object({
|
|
83
|
-
type: z.enum(['previous_period', 'previous_year']),
|
|
84
|
-
})
|
|
85
|
-
.optional(),
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
const widgetDataItemSchema = z.object({
|
|
89
|
-
groupKey: z.unknown(),
|
|
90
|
-
groupLabel: z.string().optional(),
|
|
91
|
-
value: z.number().nullable(),
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
const widgetDataResponseSchema = z.object({
|
|
95
|
-
value: z.number().nullable(),
|
|
96
|
-
data: z.array(widgetDataItemSchema),
|
|
97
|
-
comparison: z
|
|
98
|
-
.object({
|
|
99
|
-
value: z.number().nullable(),
|
|
100
|
-
change: z.number(),
|
|
101
|
-
direction: z.enum(['up', 'down', 'unchanged']),
|
|
102
|
-
})
|
|
103
|
-
.optional(),
|
|
104
|
-
metadata: z.object({
|
|
105
|
-
fetchedAt: z.string(),
|
|
106
|
-
recordCount: z.number(),
|
|
107
|
-
}),
|
|
108
|
-
})
|
|
109
|
-
|
|
110
21
|
export async function POST(req: Request) {
|
|
111
22
|
const auth = await getAuthFromRequest(req)
|
|
112
23
|
if (!auth) {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const aggregateFunctionSchema = z.enum(['count', 'sum', 'avg', 'min', 'max'])
|
|
4
|
+
export const dateGranularitySchema = z.enum(['day', 'week', 'month', 'quarter', 'year'])
|
|
5
|
+
export const dateRangePresetSchema = z.enum([
|
|
6
|
+
'today',
|
|
7
|
+
'yesterday',
|
|
8
|
+
'this_week',
|
|
9
|
+
'last_week',
|
|
10
|
+
'this_month',
|
|
11
|
+
'last_month',
|
|
12
|
+
'this_quarter',
|
|
13
|
+
'last_quarter',
|
|
14
|
+
'this_year',
|
|
15
|
+
'last_year',
|
|
16
|
+
'last_7_days',
|
|
17
|
+
'last_30_days',
|
|
18
|
+
'last_90_days',
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
export const filterOperatorSchema = z.enum([
|
|
22
|
+
'eq',
|
|
23
|
+
'neq',
|
|
24
|
+
'gt',
|
|
25
|
+
'gte',
|
|
26
|
+
'lt',
|
|
27
|
+
'lte',
|
|
28
|
+
'in',
|
|
29
|
+
'not_in',
|
|
30
|
+
'is_null',
|
|
31
|
+
'is_not_null',
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
export const widgetDataRequestSchema = z.object({
|
|
35
|
+
entityType: z.string().min(1),
|
|
36
|
+
metric: z.object({
|
|
37
|
+
field: z.string().min(1),
|
|
38
|
+
aggregate: aggregateFunctionSchema,
|
|
39
|
+
}),
|
|
40
|
+
groupBy: z
|
|
41
|
+
.object({
|
|
42
|
+
field: z.string().min(1),
|
|
43
|
+
granularity: dateGranularitySchema.optional(),
|
|
44
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
45
|
+
resolveLabels: z.boolean().optional(),
|
|
46
|
+
})
|
|
47
|
+
.optional(),
|
|
48
|
+
filters: z
|
|
49
|
+
.array(
|
|
50
|
+
z.object({
|
|
51
|
+
field: z.string().min(1),
|
|
52
|
+
operator: filterOperatorSchema,
|
|
53
|
+
value: z.unknown().optional(),
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
.optional(),
|
|
57
|
+
dateRange: z
|
|
58
|
+
.object({
|
|
59
|
+
field: z.string().min(1),
|
|
60
|
+
preset: dateRangePresetSchema,
|
|
61
|
+
})
|
|
62
|
+
.optional(),
|
|
63
|
+
comparison: z
|
|
64
|
+
.object({
|
|
65
|
+
type: z.enum(['previous_period', 'previous_year']),
|
|
66
|
+
})
|
|
67
|
+
.optional(),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
export const widgetDataItemSchema = z.object({
|
|
71
|
+
groupKey: z.unknown(),
|
|
72
|
+
groupLabel: z.string().optional(),
|
|
73
|
+
value: z.number().nullable(),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
export const widgetDataResponseSchema = z.object({
|
|
77
|
+
value: z.number().nullable(),
|
|
78
|
+
data: z.array(widgetDataItemSchema),
|
|
79
|
+
comparison: z
|
|
80
|
+
.object({
|
|
81
|
+
value: z.number().nullable(),
|
|
82
|
+
change: z.number(),
|
|
83
|
+
direction: z.enum(['up', 'down', 'unchanged']),
|
|
84
|
+
})
|
|
85
|
+
.optional(),
|
|
86
|
+
metadata: z.object({
|
|
87
|
+
fetchedAt: z.string(),
|
|
88
|
+
recordCount: z.number(),
|
|
89
|
+
}),
|
|
90
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { WidgetDataRequest, WidgetDataResponse } from '../services/widgetDataService'
|
|
2
|
+
|
|
3
|
+
export type WidgetDataBatchEntry = {
|
|
4
|
+
id: string
|
|
5
|
+
request: WidgetDataRequest
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type WidgetDataBatchResult =
|
|
9
|
+
| { id: string; ok: true; data: WidgetDataResponse }
|
|
10
|
+
| { id: string; ok: false; error: string }
|
|
11
|
+
|
|
12
|
+
export type WidgetDataBatchDeps = {
|
|
13
|
+
getRequiredFeatures: (entityType: string) => string[] | null
|
|
14
|
+
checkFeatures: (features: string[]) => Promise<boolean>
|
|
15
|
+
fetchOne: (request: WidgetDataRequest) => Promise<WidgetDataResponse>
|
|
16
|
+
describeError: (error: unknown) => string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves per-entity-type feature access for a batch of widget requests while
|
|
21
|
+
* collapsing the common case to a single RBAC resolution. The happy path checks
|
|
22
|
+
* the union of all required features once; only when the union check fails do we
|
|
23
|
+
* fall back to per-entity-type checks so a single privileged entity type does
|
|
24
|
+
* not reject widgets the caller is allowed to see.
|
|
25
|
+
*/
|
|
26
|
+
export async function resolveEntityFeatureAccess(
|
|
27
|
+
entityTypes: string[],
|
|
28
|
+
getRequiredFeatures: (entityType: string) => string[] | null,
|
|
29
|
+
checkFeatures: (features: string[]) => Promise<boolean>,
|
|
30
|
+
): Promise<Map<string, boolean>> {
|
|
31
|
+
const access = new Map<string, boolean>()
|
|
32
|
+
const featuresByEntity = new Map<string, string[]>()
|
|
33
|
+
const unionFeatures = new Set<string>()
|
|
34
|
+
|
|
35
|
+
for (const entityType of new Set(entityTypes)) {
|
|
36
|
+
const features = getRequiredFeatures(entityType) ?? []
|
|
37
|
+
featuresByEntity.set(entityType, features)
|
|
38
|
+
if (features.length === 0) {
|
|
39
|
+
access.set(entityType, true)
|
|
40
|
+
} else {
|
|
41
|
+
for (const feature of features) unionFeatures.add(feature)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const gated = [...featuresByEntity.entries()].filter(([, features]) => features.length > 0)
|
|
46
|
+
if (gated.length === 0) return access
|
|
47
|
+
|
|
48
|
+
if (await checkFeatures([...unionFeatures])) {
|
|
49
|
+
for (const [entityType] of gated) access.set(entityType, true)
|
|
50
|
+
return access
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const [entityType, features] of gated) {
|
|
54
|
+
access.set(entityType, await checkFeatures(features))
|
|
55
|
+
}
|
|
56
|
+
return access
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Runs a batch of widget-data requests against shared request-scoped
|
|
61
|
+
* dependencies (a single container, RBAC resolution, org-scope, and EM fork).
|
|
62
|
+
* Feature access is resolved once up front; each request is then executed
|
|
63
|
+
* concurrently with per-widget error isolation so one bad request never fails
|
|
64
|
+
* the whole batch.
|
|
65
|
+
*/
|
|
66
|
+
export async function runWidgetDataBatch(
|
|
67
|
+
entries: WidgetDataBatchEntry[],
|
|
68
|
+
deps: WidgetDataBatchDeps,
|
|
69
|
+
): Promise<WidgetDataBatchResult[]> {
|
|
70
|
+
const access = await resolveEntityFeatureAccess(
|
|
71
|
+
entries.map((entry) => entry.request.entityType),
|
|
72
|
+
deps.getRequiredFeatures,
|
|
73
|
+
deps.checkFeatures,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return Promise.all(
|
|
77
|
+
entries.map(async (entry): Promise<WidgetDataBatchResult> => {
|
|
78
|
+
if (access.get(entry.request.entityType) === false) {
|
|
79
|
+
return { id: entry.id, ok: false, error: 'Forbidden' }
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const data = await deps.fetchOne(entry.request)
|
|
83
|
+
return { id: entry.id, ok: true, data }
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return { id: entry.id, ok: false, error: deps.describeError(error) }
|
|
86
|
+
}
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
5
|
-
import {
|
|
5
|
+
import { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'
|
|
6
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts'
|
|
8
8
|
import {
|
|
@@ -15,7 +15,7 @@ import { DEFAULT_SETTINGS, hydrateSettings, type AovKpiSettings } from './config
|
|
|
15
15
|
import type { WidgetDataResponse } from '../../../services/widgetDataService'
|
|
16
16
|
import { formatCurrencyWithDecimals } from '../../../lib/formatters'
|
|
17
17
|
|
|
18
|
-
async function fetchAovData(settings: AovKpiSettings): Promise<WidgetDataResponse> {
|
|
18
|
+
async function fetchAovData(settings: AovKpiSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {
|
|
19
19
|
const body = {
|
|
20
20
|
entityType: 'sales:orders',
|
|
21
21
|
metric: {
|
|
@@ -29,18 +29,7 @@ async function fetchAovData(settings: AovKpiSettings): Promise<WidgetDataRespons
|
|
|
29
29
|
comparison: settings.showComparison ? { type: 'previous_period' } : undefined,
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
method: 'POST',
|
|
34
|
-
headers: { 'Content-Type': 'application/json' },
|
|
35
|
-
body: JSON.stringify(body),
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
if (!call.ok) {
|
|
39
|
-
const errorMsg = (call.result as Record<string, unknown>)?.error
|
|
40
|
-
throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch AOV data')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return call.result as WidgetDataResponse
|
|
32
|
+
return fetchWidgetData<WidgetDataResponse>(body)
|
|
44
33
|
}
|
|
45
34
|
|
|
46
35
|
const AovKpiWidget: React.FC<DashboardWidgetComponentProps<AovKpiSettings>> = ({
|
|
@@ -57,12 +46,13 @@ const AovKpiWidget: React.FC<DashboardWidgetComponentProps<AovKpiSettings>> = ({
|
|
|
57
46
|
const [loading, setLoading] = React.useState(true)
|
|
58
47
|
const [error, setError] = React.useState<string | null>(null)
|
|
59
48
|
|
|
49
|
+
const fetchWidgetData = useWidgetData()
|
|
60
50
|
const refresh = React.useCallback(async () => {
|
|
61
51
|
onRefreshStateChange?.(true)
|
|
62
52
|
setLoading(true)
|
|
63
53
|
setError(null)
|
|
64
54
|
try {
|
|
65
|
-
const data = await fetchAovData(hydrated)
|
|
55
|
+
const data = await fetchAovData(hydrated, fetchWidgetData)
|
|
66
56
|
setValue(data.value)
|
|
67
57
|
if (data.comparison) {
|
|
68
58
|
setTrend({
|
|
@@ -79,7 +69,7 @@ const AovKpiWidget: React.FC<DashboardWidgetComponentProps<AovKpiSettings>> = ({
|
|
|
79
69
|
setLoading(false)
|
|
80
70
|
onRefreshStateChange?.(false)
|
|
81
71
|
}
|
|
82
|
-
}, [hydrated, onRefreshStateChange, t])
|
|
72
|
+
}, [hydrated, fetchWidgetData, onRefreshStateChange, t])
|
|
83
73
|
|
|
84
74
|
React.useEffect(() => {
|
|
85
75
|
refresh().catch(() => {})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
5
|
-
import {
|
|
5
|
+
import { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'
|
|
6
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts'
|
|
8
8
|
import {
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { DEFAULT_SETTINGS, hydrateSettings, type NewCustomersKpiSettings } from './config'
|
|
15
15
|
import type { WidgetDataResponse } from '../../../services/widgetDataService'
|
|
16
16
|
|
|
17
|
-
async function fetchNewCustomersData(settings: NewCustomersKpiSettings): Promise<WidgetDataResponse> {
|
|
17
|
+
async function fetchNewCustomersData(settings: NewCustomersKpiSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {
|
|
18
18
|
const body = {
|
|
19
19
|
entityType: 'customers:entities',
|
|
20
20
|
metric: {
|
|
@@ -28,18 +28,7 @@ async function fetchNewCustomersData(settings: NewCustomersKpiSettings): Promise
|
|
|
28
28
|
comparison: settings.showComparison ? { type: 'previous_period' } : undefined,
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
method: 'POST',
|
|
33
|
-
headers: { 'Content-Type': 'application/json' },
|
|
34
|
-
body: JSON.stringify(body),
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
if (!call.ok) {
|
|
38
|
-
const errorMsg = (call.result as Record<string, unknown>)?.error
|
|
39
|
-
throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch new customers data')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return call.result as WidgetDataResponse
|
|
31
|
+
return fetchWidgetData<WidgetDataResponse>(body)
|
|
43
32
|
}
|
|
44
33
|
|
|
45
34
|
const NewCustomersKpiWidget: React.FC<DashboardWidgetComponentProps<NewCustomersKpiSettings>> = ({
|
|
@@ -56,12 +45,13 @@ const NewCustomersKpiWidget: React.FC<DashboardWidgetComponentProps<NewCustomers
|
|
|
56
45
|
const [loading, setLoading] = React.useState(true)
|
|
57
46
|
const [error, setError] = React.useState<string | null>(null)
|
|
58
47
|
|
|
48
|
+
const fetchWidgetData = useWidgetData()
|
|
59
49
|
const refresh = React.useCallback(async () => {
|
|
60
50
|
onRefreshStateChange?.(true)
|
|
61
51
|
setLoading(true)
|
|
62
52
|
setError(null)
|
|
63
53
|
try {
|
|
64
|
-
const data = await fetchNewCustomersData(hydrated)
|
|
54
|
+
const data = await fetchNewCustomersData(hydrated, fetchWidgetData)
|
|
65
55
|
setValue(data.value)
|
|
66
56
|
if (data.comparison) {
|
|
67
57
|
setTrend({
|
|
@@ -78,7 +68,7 @@ const NewCustomersKpiWidget: React.FC<DashboardWidgetComponentProps<NewCustomers
|
|
|
78
68
|
setLoading(false)
|
|
79
69
|
onRefreshStateChange?.(false)
|
|
80
70
|
}
|
|
81
|
-
}, [hydrated, onRefreshStateChange, t])
|
|
71
|
+
}, [hydrated, fetchWidgetData, onRefreshStateChange, t])
|
|
82
72
|
|
|
83
73
|
React.useEffect(() => {
|
|
84
74
|
refresh().catch(() => {})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
5
|
-
import {
|
|
5
|
+
import { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'
|
|
6
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { PieChart, type PieChartDataItem } from '@open-mercato/ui/backend/charts'
|
|
8
8
|
import {
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import { DEFAULT_SETTINGS, hydrateSettings, type OrdersByStatusSettings } from './config'
|
|
21
21
|
import type { WidgetDataResponse } from '../../../services/widgetDataService'
|
|
22
22
|
|
|
23
|
-
async function fetchOrdersByStatusData(settings: OrdersByStatusSettings): Promise<WidgetDataResponse> {
|
|
23
|
+
async function fetchOrdersByStatusData(settings: OrdersByStatusSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {
|
|
24
24
|
const body = {
|
|
25
25
|
entityType: 'sales:orders',
|
|
26
26
|
metric: {
|
|
@@ -36,18 +36,7 @@ async function fetchOrdersByStatusData(settings: OrdersByStatusSettings): Promis
|
|
|
36
36
|
},
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
method: 'POST',
|
|
41
|
-
headers: { 'Content-Type': 'application/json' },
|
|
42
|
-
body: JSON.stringify(body),
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
if (!call.ok) {
|
|
46
|
-
const errorMsg = (call.result as Record<string, unknown>)?.error
|
|
47
|
-
throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch orders by status data')
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return call.result as WidgetDataResponse
|
|
39
|
+
return fetchWidgetData<WidgetDataResponse>(body)
|
|
51
40
|
}
|
|
52
41
|
|
|
53
42
|
const ORDER_STATUS_KEYS: Record<string, string> = {
|
|
@@ -82,12 +71,13 @@ const OrdersByStatusWidget: React.FC<DashboardWidgetComponentProps<OrdersByStatu
|
|
|
82
71
|
const [loading, setLoading] = React.useState(true)
|
|
83
72
|
const [error, setError] = React.useState<string | null>(null)
|
|
84
73
|
|
|
74
|
+
const fetchWidgetData = useWidgetData()
|
|
85
75
|
const refresh = React.useCallback(async () => {
|
|
86
76
|
onRefreshStateChange?.(true)
|
|
87
77
|
setLoading(true)
|
|
88
78
|
setError(null)
|
|
89
79
|
try {
|
|
90
|
-
const result = await fetchOrdersByStatusData(hydrated)
|
|
80
|
+
const result = await fetchOrdersByStatusData(hydrated, fetchWidgetData)
|
|
91
81
|
const chartData = result.data.map((item) => ({
|
|
92
82
|
name: formatStatusLabel(item.groupKey as string | null, t),
|
|
93
83
|
value: item.value ?? 0,
|
|
@@ -100,7 +90,7 @@ const OrdersByStatusWidget: React.FC<DashboardWidgetComponentProps<OrdersByStatu
|
|
|
100
90
|
setLoading(false)
|
|
101
91
|
onRefreshStateChange?.(false)
|
|
102
92
|
}
|
|
103
|
-
}, [hydrated, onRefreshStateChange, t])
|
|
93
|
+
}, [hydrated, fetchWidgetData, onRefreshStateChange, t])
|
|
104
94
|
|
|
105
95
|
React.useEffect(() => {
|
|
106
96
|
refresh().catch(() => {})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
5
|
-
import {
|
|
5
|
+
import { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'
|
|
6
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts'
|
|
8
8
|
import {
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { DEFAULT_SETTINGS, hydrateSettings, type OrdersKpiSettings } from './config'
|
|
15
15
|
import type { WidgetDataResponse } from '../../../services/widgetDataService'
|
|
16
16
|
|
|
17
|
-
async function fetchOrdersData(settings: OrdersKpiSettings): Promise<WidgetDataResponse> {
|
|
17
|
+
async function fetchOrdersData(settings: OrdersKpiSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {
|
|
18
18
|
const body = {
|
|
19
19
|
entityType: 'sales:orders',
|
|
20
20
|
metric: {
|
|
@@ -28,18 +28,7 @@ async function fetchOrdersData(settings: OrdersKpiSettings): Promise<WidgetDataR
|
|
|
28
28
|
comparison: settings.showComparison ? { type: 'previous_period' } : undefined,
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
method: 'POST',
|
|
33
|
-
headers: { 'Content-Type': 'application/json' },
|
|
34
|
-
body: JSON.stringify(body),
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
if (!call.ok) {
|
|
38
|
-
const errorMsg = (call.result as Record<string, unknown>)?.error
|
|
39
|
-
throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch orders data')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return call.result as WidgetDataResponse
|
|
31
|
+
return fetchWidgetData<WidgetDataResponse>(body)
|
|
43
32
|
}
|
|
44
33
|
|
|
45
34
|
const OrdersKpiWidget: React.FC<DashboardWidgetComponentProps<OrdersKpiSettings>> = ({
|
|
@@ -56,12 +45,13 @@ const OrdersKpiWidget: React.FC<DashboardWidgetComponentProps<OrdersKpiSettings>
|
|
|
56
45
|
const [loading, setLoading] = React.useState(true)
|
|
57
46
|
const [error, setError] = React.useState<string | null>(null)
|
|
58
47
|
|
|
48
|
+
const fetchWidgetData = useWidgetData()
|
|
59
49
|
const refresh = React.useCallback(async () => {
|
|
60
50
|
onRefreshStateChange?.(true)
|
|
61
51
|
setLoading(true)
|
|
62
52
|
setError(null)
|
|
63
53
|
try {
|
|
64
|
-
const data = await fetchOrdersData(hydrated)
|
|
54
|
+
const data = await fetchOrdersData(hydrated, fetchWidgetData)
|
|
65
55
|
setValue(data.value)
|
|
66
56
|
if (data.comparison) {
|
|
67
57
|
setTrend({
|
|
@@ -78,7 +68,7 @@ const OrdersKpiWidget: React.FC<DashboardWidgetComponentProps<OrdersKpiSettings>
|
|
|
78
68
|
setLoading(false)
|
|
79
69
|
onRefreshStateChange?.(false)
|
|
80
70
|
}
|
|
81
|
-
}, [hydrated, onRefreshStateChange, t])
|
|
71
|
+
}, [hydrated, fetchWidgetData, onRefreshStateChange, t])
|
|
82
72
|
|
|
83
73
|
React.useEffect(() => {
|
|
84
74
|
refresh().catch(() => {})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
5
|
-
import {
|
|
5
|
+
import { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'
|
|
6
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'
|
|
8
8
|
import {
|
|
@@ -14,7 +14,7 @@ import { DEFAULT_SETTINGS, hydrateSettings, type PipelineSummarySettings } from
|
|
|
14
14
|
import type { WidgetDataResponse } from '../../../services/widgetDataService'
|
|
15
15
|
import { formatCurrencyCompact } from '../../../lib/formatters'
|
|
16
16
|
|
|
17
|
-
async function fetchPipelineData(settings: PipelineSummarySettings): Promise<WidgetDataResponse> {
|
|
17
|
+
async function fetchPipelineData(settings: PipelineSummarySettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {
|
|
18
18
|
const body = {
|
|
19
19
|
entityType: 'customers:deals',
|
|
20
20
|
metric: {
|
|
@@ -31,18 +31,7 @@ async function fetchPipelineData(settings: PipelineSummarySettings): Promise<Wid
|
|
|
31
31
|
},
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
method: 'POST',
|
|
36
|
-
headers: { 'Content-Type': 'application/json' },
|
|
37
|
-
body: JSON.stringify(body),
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
if (!call.ok) {
|
|
41
|
-
const errorMsg = (call.result as Record<string, unknown>)?.error
|
|
42
|
-
throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch pipeline data')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return call.result as WidgetDataResponse
|
|
34
|
+
return fetchWidgetData<WidgetDataResponse>(body)
|
|
46
35
|
}
|
|
47
36
|
|
|
48
37
|
function formatStageLabel(stage: unknown, t: (key: string, fallback: string) => string): string {
|
|
@@ -69,12 +58,13 @@ const PipelineSummaryWidget: React.FC<DashboardWidgetComponentProps<PipelineSumm
|
|
|
69
58
|
const [loading, setLoading] = React.useState(true)
|
|
70
59
|
const [error, setError] = React.useState<string | null>(null)
|
|
71
60
|
|
|
61
|
+
const fetchWidgetData = useWidgetData()
|
|
72
62
|
const refresh = React.useCallback(async () => {
|
|
73
63
|
onRefreshStateChange?.(true)
|
|
74
64
|
setLoading(true)
|
|
75
65
|
setError(null)
|
|
76
66
|
try {
|
|
77
|
-
const result = await fetchPipelineData(hydrated)
|
|
67
|
+
const result = await fetchPipelineData(hydrated, fetchWidgetData)
|
|
78
68
|
const chartData = result.data
|
|
79
69
|
.filter((item) => item.groupKey != null && item.groupKey !== '' && String(item.groupKey) !== '0')
|
|
80
70
|
.map((item) => ({
|
|
@@ -89,7 +79,7 @@ const PipelineSummaryWidget: React.FC<DashboardWidgetComponentProps<PipelineSumm
|
|
|
89
79
|
setLoading(false)
|
|
90
80
|
onRefreshStateChange?.(false)
|
|
91
81
|
}
|
|
92
|
-
}, [hydrated, onRefreshStateChange, t])
|
|
82
|
+
}, [hydrated, fetchWidgetData, onRefreshStateChange, t])
|
|
93
83
|
|
|
94
84
|
React.useEffect(() => {
|
|
95
85
|
refresh().catch(() => {})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
5
|
-
import {
|
|
5
|
+
import { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'
|
|
6
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts'
|
|
8
8
|
import {
|
|
@@ -15,7 +15,7 @@ import { DEFAULT_SETTINGS, hydrateSettings, type RevenueKpiSettings } from './co
|
|
|
15
15
|
import type { WidgetDataResponse } from '../../../services/widgetDataService'
|
|
16
16
|
import { formatCurrency } from '../../../lib/formatters'
|
|
17
17
|
|
|
18
|
-
async function fetchRevenueData(settings: RevenueKpiSettings): Promise<WidgetDataResponse> {
|
|
18
|
+
async function fetchRevenueData(settings: RevenueKpiSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {
|
|
19
19
|
const body = {
|
|
20
20
|
entityType: 'sales:orders',
|
|
21
21
|
metric: {
|
|
@@ -29,18 +29,7 @@ async function fetchRevenueData(settings: RevenueKpiSettings): Promise<WidgetDat
|
|
|
29
29
|
comparison: settings.showComparison ? { type: 'previous_period' } : undefined,
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
method: 'POST',
|
|
34
|
-
headers: { 'Content-Type': 'application/json' },
|
|
35
|
-
body: JSON.stringify(body),
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
if (!call.ok) {
|
|
39
|
-
const errorMsg = (call.result as Record<string, unknown>)?.error
|
|
40
|
-
throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch revenue data')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return call.result as WidgetDataResponse
|
|
32
|
+
return fetchWidgetData<WidgetDataResponse>(body)
|
|
44
33
|
}
|
|
45
34
|
|
|
46
35
|
const RevenueKpiWidget: React.FC<DashboardWidgetComponentProps<RevenueKpiSettings>> = ({
|
|
@@ -57,12 +46,13 @@ const RevenueKpiWidget: React.FC<DashboardWidgetComponentProps<RevenueKpiSetting
|
|
|
57
46
|
const [loading, setLoading] = React.useState(true)
|
|
58
47
|
const [error, setError] = React.useState<string | null>(null)
|
|
59
48
|
|
|
49
|
+
const fetchWidgetData = useWidgetData()
|
|
60
50
|
const refresh = React.useCallback(async () => {
|
|
61
51
|
onRefreshStateChange?.(true)
|
|
62
52
|
setLoading(true)
|
|
63
53
|
setError(null)
|
|
64
54
|
try {
|
|
65
|
-
const data = await fetchRevenueData(hydrated)
|
|
55
|
+
const data = await fetchRevenueData(hydrated, fetchWidgetData)
|
|
66
56
|
setValue(data.value)
|
|
67
57
|
if (data.comparison) {
|
|
68
58
|
setTrend({
|
|
@@ -79,7 +69,7 @@ const RevenueKpiWidget: React.FC<DashboardWidgetComponentProps<RevenueKpiSetting
|
|
|
79
69
|
setLoading(false)
|
|
80
70
|
onRefreshStateChange?.(false)
|
|
81
71
|
}
|
|
82
|
-
}, [hydrated, onRefreshStateChange, t])
|
|
72
|
+
}, [hydrated, fetchWidgetData, onRefreshStateChange, t])
|
|
83
73
|
|
|
84
74
|
React.useEffect(() => {
|
|
85
75
|
refresh().catch(() => {})
|