@open-mercato/core 0.6.4-develop.4121.1.0d7f20d229 → 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.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/dashboards/api/widgets/data/batch/route.js +137 -0
  3. package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +7 -0
  4. package/dist/modules/dashboards/api/widgets/data/route.js +1 -75
  5. package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
  6. package/dist/modules/dashboards/api/widgets/data/schema.js +85 -0
  7. package/dist/modules/dashboards/api/widgets/data/schema.js.map +7 -0
  8. package/dist/modules/dashboards/lib/widgetDataBatch.js +49 -0
  9. package/dist/modules/dashboards/lib/widgetDataBatch.js.map +7 -0
  10. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +6 -14
  11. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +2 -2
  12. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +6 -14
  13. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +2 -2
  14. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +6 -14
  15. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +2 -2
  16. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +6 -14
  17. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +2 -2
  18. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +6 -14
  19. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
  20. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +6 -14
  21. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +2 -2
  22. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +6 -14
  23. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +2 -2
  24. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +6 -14
  25. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +2 -2
  26. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +6 -14
  27. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +2 -2
  28. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +6 -14
  29. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +2 -2
  30. package/package.json +7 -7
  31. package/src/modules/dashboards/api/widgets/data/batch/route.ts +168 -0
  32. package/src/modules/dashboards/api/widgets/data/route.ts +1 -90
  33. package/src/modules/dashboards/api/widgets/data/schema.ts +90 -0
  34. package/src/modules/dashboards/lib/widgetDataBatch.ts +89 -0
  35. package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +6 -16
  36. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +6 -16
  37. package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +6 -16
  38. package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +6 -16
  39. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +6 -16
  40. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +6 -16
  41. package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +6 -16
  42. package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +6 -16
  43. package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +6 -16
  44. package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +6 -16
@@ -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 { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
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
- const call = await apiCall<WidgetDataResponse>('/api/dashboards/widgets/data', {
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 { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
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
- const call = await apiCall<WidgetDataResponse>('/api/dashboards/widgets/data', {
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 { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
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
- const call = await apiCall<WidgetDataResponse>('/api/dashboards/widgets/data', {
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 { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
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
- const call = await apiCall<WidgetDataResponse>('/api/dashboards/widgets/data', {
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 { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
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
- const call = await apiCall<WidgetDataResponse>('/api/dashboards/widgets/data', {
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 { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
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
- const call = await apiCall<WidgetDataResponse>('/api/dashboards/widgets/data', {
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(() => {})
@@ -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 { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
5
+ import { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'
6
6
  import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
7
7
  import { LineChart, type LineChartDataItem } from '@open-mercato/ui/backend/charts'
8
8
  import {
@@ -22,7 +22,7 @@ import { DEFAULT_SETTINGS, hydrateSettings, type RevenueTrendSettings } from './
22
22
  import type { WidgetDataResponse } from '../../../services/widgetDataService'
23
23
  import { formatCurrencyCompact } from '../../../lib/formatters'
24
24
 
25
- async function fetchRevenueTrendData(settings: RevenueTrendSettings): Promise<WidgetDataResponse> {
25
+ async function fetchRevenueTrendData(settings: RevenueTrendSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {
26
26
  const body = {
27
27
  entityType: 'sales:orders',
28
28
  metric: {
@@ -39,18 +39,7 @@ async function fetchRevenueTrendData(settings: RevenueTrendSettings): Promise<Wi
39
39
  },
40
40
  }
41
41
 
42
- const call = await apiCall<WidgetDataResponse>('/api/dashboards/widgets/data', {
43
- method: 'POST',
44
- headers: { 'Content-Type': 'application/json' },
45
- body: JSON.stringify(body),
46
- })
47
-
48
- if (!call.ok) {
49
- const errorMsg = (call.result as Record<string, unknown>)?.error
50
- throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch revenue trend data')
51
- }
52
-
53
- return call.result as WidgetDataResponse
42
+ return fetchWidgetData<WidgetDataResponse>(body)
54
43
  }
55
44
 
56
45
  function formatDate(dateStr: string | null, granularity: DateGranularity, locale?: string): string {
@@ -125,12 +114,13 @@ const RevenueTrendWidget: React.FC<DashboardWidgetComponentProps<RevenueTrendSet
125
114
  const [loading, setLoading] = React.useState(true)
126
115
  const [error, setError] = React.useState<string | null>(null)
127
116
 
117
+ const fetchWidgetData = useWidgetData()
128
118
  const refresh = React.useCallback(async () => {
129
119
  onRefreshStateChange?.(true)
130
120
  setLoading(true)
131
121
  setError(null)
132
122
  try {
133
- const result = await fetchRevenueTrendData(hydrated)
123
+ const result = await fetchRevenueTrendData(hydrated, fetchWidgetData)
134
124
  const sortedData = [...result.data].sort((a, b) => {
135
125
  const aTime = new Date(a.groupKey as string || 0).getTime()
136
126
  const bTime = new Date(b.groupKey as string || 0).getTime()
@@ -148,7 +138,7 @@ const RevenueTrendWidget: React.FC<DashboardWidgetComponentProps<RevenueTrendSet
148
138
  setLoading(false)
149
139
  onRefreshStateChange?.(false)
150
140
  }
151
- }, [hydrated, locale, onRefreshStateChange, t])
141
+ }, [hydrated, fetchWidgetData, locale, onRefreshStateChange, t])
152
142
 
153
143
  React.useEffect(() => {
154
144
  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 { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
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 { DateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'
@@ -11,7 +11,7 @@ import { DEFAULT_SETTINGS, hydrateSettings, type SalesByRegionSettings } from '.
11
11
  import type { WidgetDataResponse } from '../../../services/widgetDataService'
12
12
  import { formatCurrencyCompact } from '../../../lib/formatters'
13
13
 
14
- async function fetchSalesByRegionData(settings: SalesByRegionSettings): Promise<WidgetDataResponse> {
14
+ async function fetchSalesByRegionData(settings: SalesByRegionSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {
15
15
  const body = {
16
16
  entityType: 'sales:orders',
17
17
  metric: {
@@ -28,18 +28,7 @@ async function fetchSalesByRegionData(settings: SalesByRegionSettings): Promise<
28
28
  },
29
29
  }
30
30
 
31
- const call = await apiCall<WidgetDataResponse>('/api/dashboards/widgets/data', {
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 sales by region data')
40
- }
41
-
42
- return call.result as WidgetDataResponse
31
+ return fetchWidgetData<WidgetDataResponse>(body)
43
32
  }
44
33
 
45
34
  const SalesByRegionWidget: React.FC<DashboardWidgetComponentProps<SalesByRegionSettings>> = ({
@@ -55,12 +44,13 @@ const SalesByRegionWidget: React.FC<DashboardWidgetComponentProps<SalesByRegionS
55
44
  const [loading, setLoading] = React.useState(true)
56
45
  const [error, setError] = React.useState<string | null>(null)
57
46
 
47
+ const fetchWidgetData = useWidgetData()
58
48
  const refresh = React.useCallback(async () => {
59
49
  onRefreshStateChange?.(true)
60
50
  setLoading(true)
61
51
  setError(null)
62
52
  try {
63
- const result = await fetchSalesByRegionData(hydrated)
53
+ const result = await fetchSalesByRegionData(hydrated, fetchWidgetData)
64
54
  const chartData = result.data.map((item) => ({
65
55
  region: String(item.groupKey || t('dashboards.analytics.labels.unknown', 'Unknown')),
66
56
  Revenue: item.value ?? 0,
@@ -73,7 +63,7 @@ const SalesByRegionWidget: React.FC<DashboardWidgetComponentProps<SalesByRegionS
73
63
  setLoading(false)
74
64
  onRefreshStateChange?.(false)
75
65
  }
76
- }, [hydrated, onRefreshStateChange, t])
66
+ }, [hydrated, fetchWidgetData, onRefreshStateChange, t])
77
67
 
78
68
  React.useEffect(() => {
79
69
  refresh().catch(() => {})