@open-mercato/core 0.6.4-develop.4121.1.0d7f20d229 → 0.6.4-develop.4152.1.1c429e5200

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 (72) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customer_accounts/api/admin/users/[id]/reset-password.js +1 -0
  3. package/dist/modules/customer_accounts/api/admin/users/[id]/reset-password.js.map +2 -2
  4. package/dist/modules/customer_accounts/api/admin/users/[id]/send-reset-link.js +1 -0
  5. package/dist/modules/customer_accounts/api/admin/users/[id]/send-reset-link.js.map +2 -2
  6. package/dist/modules/customer_accounts/api/admin/users/[id]/verify-email.js +1 -0
  7. package/dist/modules/customer_accounts/api/admin/users/[id]/verify-email.js.map +2 -2
  8. package/dist/modules/customer_accounts/api/admin/users/[id].js +1 -0
  9. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
  10. package/dist/modules/customer_accounts/api/email/verify.js +1 -0
  11. package/dist/modules/customer_accounts/api/email/verify.js.map +2 -2
  12. package/dist/modules/customer_accounts/api/portal/events/stream.js +20 -2
  13. package/dist/modules/customer_accounts/api/portal/events/stream.js.map +2 -2
  14. package/dist/modules/dashboards/api/widgets/data/batch/route.js +137 -0
  15. package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +7 -0
  16. package/dist/modules/dashboards/api/widgets/data/route.js +1 -75
  17. package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
  18. package/dist/modules/dashboards/api/widgets/data/schema.js +85 -0
  19. package/dist/modules/dashboards/api/widgets/data/schema.js.map +7 -0
  20. package/dist/modules/dashboards/lib/widgetDataBatch.js +49 -0
  21. package/dist/modules/dashboards/lib/widgetDataBatch.js.map +7 -0
  22. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +6 -14
  23. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +2 -2
  24. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +6 -14
  25. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +2 -2
  26. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +6 -14
  27. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +2 -2
  28. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +6 -14
  29. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +2 -2
  30. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +6 -14
  31. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
  32. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +6 -14
  33. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +2 -2
  34. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +6 -14
  35. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +2 -2
  36. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +6 -14
  37. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +2 -2
  38. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +6 -14
  39. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +2 -2
  40. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +6 -14
  41. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +2 -2
  42. package/dist/modules/planner/components/AvailabilityRulesEditor.js +19 -13
  43. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  44. package/dist/modules/planner/components/availabilityRulesEditorState.js +10 -1
  45. package/dist/modules/planner/components/availabilityRulesEditorState.js.map +2 -2
  46. package/package.json +7 -7
  47. package/src/modules/customer_accounts/api/admin/users/[id]/reset-password.ts +1 -0
  48. package/src/modules/customer_accounts/api/admin/users/[id]/send-reset-link.ts +1 -0
  49. package/src/modules/customer_accounts/api/admin/users/[id]/verify-email.ts +1 -0
  50. package/src/modules/customer_accounts/api/admin/users/[id].ts +1 -0
  51. package/src/modules/customer_accounts/api/email/verify.ts +1 -0
  52. package/src/modules/customer_accounts/api/portal/events/stream.ts +23 -2
  53. package/src/modules/dashboards/api/widgets/data/batch/route.ts +168 -0
  54. package/src/modules/dashboards/api/widgets/data/route.ts +1 -90
  55. package/src/modules/dashboards/api/widgets/data/schema.ts +90 -0
  56. package/src/modules/dashboards/lib/widgetDataBatch.ts +89 -0
  57. package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +6 -16
  58. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +6 -16
  59. package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +6 -16
  60. package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +6 -16
  61. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +6 -16
  62. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +6 -16
  63. package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +6 -16
  64. package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +6 -16
  65. package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +6 -16
  66. package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +6 -16
  67. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +20 -13
  68. package/src/modules/planner/components/availabilityRulesEditorState.ts +21 -0
  69. package/src/modules/planner/i18n/de.json +3 -3
  70. package/src/modules/planner/i18n/en.json +3 -3
  71. package/src/modules/planner/i18n/es.json +3 -3
  72. package/src/modules/planner/i18n/pl.json +3 -3
@@ -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(() => {})
@@ -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 { TopNTable, type TopNTableColumn } from '@open-mercato/ui/backend/charts'
8
8
  import { DateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'
@@ -17,7 +17,7 @@ type CustomerRow = {
17
17
  revenue: number
18
18
  }
19
19
 
20
- async function fetchTopCustomersData(settings: TopCustomersSettings): Promise<WidgetDataResponse> {
20
+ async function fetchTopCustomersData(settings: TopCustomersSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {
21
21
  const body = {
22
22
  entityType: 'sales:orders',
23
23
  metric: {
@@ -35,18 +35,7 @@ async function fetchTopCustomersData(settings: TopCustomersSettings): Promise<Wi
35
35
  },
36
36
  }
37
37
 
38
- const call = await apiCall<WidgetDataResponse>('/api/dashboards/widgets/data', {
39
- method: 'POST',
40
- headers: { 'Content-Type': 'application/json' },
41
- body: JSON.stringify(body),
42
- })
43
-
44
- if (!call.ok) {
45
- const errorMsg = (call.result as Record<string, unknown>)?.error
46
- throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch top customers data')
47
- }
48
-
49
- return call.result as WidgetDataResponse
38
+ return fetchWidgetData<WidgetDataResponse>(body)
50
39
  }
51
40
 
52
41
  function formatCustomerName(name: string | null, unknownLabel: string): string {
@@ -90,12 +79,13 @@ const TopCustomersWidget: React.FC<DashboardWidgetComponentProps<TopCustomersSet
90
79
  [t, unknownLabel],
91
80
  )
92
81
 
82
+ const fetchWidgetData = useWidgetData()
93
83
  const refresh = React.useCallback(async () => {
94
84
  onRefreshStateChange?.(true)
95
85
  setLoading(true)
96
86
  setError(null)
97
87
  try {
98
- const result = await fetchTopCustomersData(hydrated)
88
+ const result = await fetchTopCustomersData(hydrated, fetchWidgetData)
99
89
  const tableData: CustomerRow[] = result.data.map((item, index) => ({
100
90
  rank: index + 1,
101
91
  customerId: item.groupLabel || String(item.groupKey || t('dashboards.analytics.labels.unknown', 'Unknown')),
@@ -109,7 +99,7 @@ const TopCustomersWidget: React.FC<DashboardWidgetComponentProps<TopCustomersSet
109
99
  setLoading(false)
110
100
  onRefreshStateChange?.(false)
111
101
  }
112
- }, [hydrated, onRefreshStateChange, t])
102
+ }, [hydrated, fetchWidgetData, onRefreshStateChange, t])
113
103
 
114
104
  React.useEffect(() => {
115
105
  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, InlineDateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'
@@ -18,7 +18,7 @@ import { DEFAULT_SETTINGS, hydrateSettings, type TopProductsSettings } from './c
18
18
  import type { WidgetDataResponse } from '../../../services/widgetDataService'
19
19
  import { formatCurrencyCompact } from '../../../lib/formatters'
20
20
 
21
- async function fetchTopProductsData(settings: TopProductsSettings): Promise<WidgetDataResponse> {
21
+ async function fetchTopProductsData(settings: TopProductsSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {
22
22
  const body = {
23
23
  entityType: 'sales:order_lines',
24
24
  metric: {
@@ -36,18 +36,7 @@ async function fetchTopProductsData(settings: TopProductsSettings): Promise<Widg
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 top products data')
48
- }
49
-
50
- return call.result as WidgetDataResponse
39
+ return fetchWidgetData<WidgetDataResponse>(body)
51
40
  }
52
41
 
53
42
  function truncateLabel(
@@ -82,6 +71,7 @@ const TopProductsWidget: React.FC<DashboardWidgetComponentProps<TopProductsSetti
82
71
  const [error, setError] = React.useState<string | null>(null)
83
72
  const fetchingRef = React.useRef(false)
84
73
 
74
+ const fetchWidgetData = useWidgetData()
85
75
  const refresh = React.useCallback(async () => {
86
76
  if (fetchingRef.current) return
87
77
  fetchingRef.current = true
@@ -89,7 +79,7 @@ const TopProductsWidget: React.FC<DashboardWidgetComponentProps<TopProductsSetti
89
79
  setLoading(true)
90
80
  setError(null)
91
81
  try {
92
- const result = await fetchTopProductsData(hydrated)
82
+ const result = await fetchTopProductsData(hydrated, fetchWidgetData)
93
83
  const chartData = result.data.map((item, index) => ({
94
84
  name: truncateLabel(item.groupLabel ?? item.groupKey ?? `Product ${index + 1}`, t),
95
85
  Revenue: item.value ?? 0,
@@ -103,7 +93,7 @@ const TopProductsWidget: React.FC<DashboardWidgetComponentProps<TopProductsSetti
103
93
  onRefreshStateChange?.(false)
104
94
  fetchingRef.current = false
105
95
  }
106
- }, [hydrated, onRefreshStateChange, t])
96
+ }, [hydrated, fetchWidgetData, onRefreshStateChange, t])
107
97
 
108
98
  React.useEffect(() => {
109
99
  refresh().catch(() => {})
@@ -30,7 +30,11 @@ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
30
30
  import { parseAvailabilityRuleWindow } from '@open-mercato/core/modules/planner/lib/availabilitySchedule'
31
31
  import { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'
32
32
  import { Calendar, Clock, List, PencilLine, Plus, Trash2 } from 'lucide-react'
33
- import { resolveRuleSetSelectValue } from './availabilityRulesEditorState'
33
+ import {
34
+ resolveRuleSetSelectValue,
35
+ requiresResetConfirmation,
36
+ selectCustomRuleIdsToDelete,
37
+ } from './availabilityRulesEditorState'
34
38
 
35
39
  type AvailabilityRepeat = 'once' | 'daily' | 'weekly'
36
40
  type AvailabilitySubjectType = 'member' | 'resource' | 'ruleset'
@@ -455,7 +459,7 @@ export function AvailabilityRulesEditor({
455
459
  ruleSetPlaceholder: t(`${labelPrefix}.availability.ruleset.placeholder`, 'Custom schedule'),
456
460
  ruleSetCustomize: t(`${labelPrefix}.availability.ruleset.customize`, 'Customize schedule'),
457
461
  ruleSetReset: t(`${labelPrefix}.availability.ruleset.reset`, 'Reset to schedule'),
458
- ruleSetConfirm: t(`${labelPrefix}.availability.ruleset.confirm`, 'Changing the schedule will reset custom hours. Continue?'),
462
+ ruleSetConfirm: t(`${labelPrefix}.availability.ruleset.confirm`, 'Resetting to the schedule will delete your custom hours. Continue?'),
459
463
  ruleSetLoading: t(`${labelPrefix}.availability.ruleset.loading`, 'Loading schedules...'),
460
464
  ruleSetError: t(`${labelPrefix}.availability.ruleset.error`, 'Failed to load schedules.'),
461
465
  ruleSetCreateLabel: t(`${labelPrefix}.availability.ruleset.create`, 'New schedule'),
@@ -957,9 +961,17 @@ export function AvailabilityRulesEditor({
957
961
  const handleResetToRuleSet = React.useCallback(async () => {
958
962
  if (isReadOnly) return
959
963
  if (!effectiveRulesetId) return
964
+ if (requiresResetConfirmation(availabilityRules)) {
965
+ const confirmed = await confirm({
966
+ title: listLabels.ruleSetConfirm,
967
+ variant: 'destructive',
968
+ })
969
+ if (!confirmed) return
970
+ }
960
971
  try {
972
+ const idsToDelete = selectCustomRuleIdsToDelete('reset', availabilityRules)
961
973
  await Promise.all(
962
- availabilityRules.map((rule) => deleteCrud('planner/availability', rule.id, { errorMessage: listLabels.saveWeeklyError })),
974
+ idsToDelete.map((id) => deleteCrud('planner/availability', id, { errorMessage: listLabels.saveWeeklyError })),
963
975
  )
964
976
  setCustomOverridesEnabled(false)
965
977
  await refreshAvailability()
@@ -967,19 +979,16 @@ export function AvailabilityRulesEditor({
967
979
  const message = error instanceof Error ? error.message : listLabels.saveWeeklyError
968
980
  flash(message, 'error')
969
981
  }
970
- }, [availabilityRules, effectiveRulesetId, listLabels.saveWeeklyError, refreshAvailability, isReadOnly])
982
+ }, [availabilityRules, confirm, effectiveRulesetId, listLabels.ruleSetConfirm, listLabels.saveWeeklyError, refreshAvailability, isReadOnly])
971
983
 
972
984
  const handleRuleSetChange = React.useCallback(async (nextId: string | null) => {
973
985
  if (isReadOnly) return
974
986
  if (!onRulesetChange) return
975
- if (availabilityRules.length > 0 && nextId !== effectiveRulesetId) {
976
- const confirmed = await confirm({
977
- title: listLabels.ruleSetConfirm,
978
- variant: 'default',
979
- })
980
- if (!confirmed) return
987
+ // Switching preserves saved custom hours (#2325); only an explicit reset clears them.
988
+ const idsToDelete = selectCustomRuleIdsToDelete('switch', availabilityRules)
989
+ if (idsToDelete.length) {
981
990
  await Promise.all(
982
- availabilityRules.map((rule) => deleteCrud('planner/availability', rule.id, { errorMessage: listLabels.saveWeeklyError })),
991
+ idsToDelete.map((id) => deleteCrud('planner/availability', id, { errorMessage: listLabels.saveWeeklyError })),
983
992
  )
984
993
  }
985
994
  setSelectedRulesetId(nextId)
@@ -993,9 +1002,7 @@ export function AvailabilityRulesEditor({
993
1002
  }
994
1003
  }, [
995
1004
  availabilityRules,
996
- confirm,
997
1005
  effectiveRulesetId,
998
- listLabels.ruleSetConfirm,
999
1006
  listLabels.saveWeeklyError,
1000
1007
  onRulesetChange,
1001
1008
  refreshAvailability,
@@ -2,6 +2,12 @@ export type AvailabilityRuleSetOption = {
2
2
  id: string
3
3
  }
4
4
 
5
+ export type AvailabilityRuleRef = {
6
+ id: string
7
+ }
8
+
9
+ export type RuleSetTransition = 'switch' | 'reset'
10
+
5
11
  export function resolveRuleSetSelectValue(
6
12
  ruleSets: AvailabilityRuleSetOption[],
7
13
  selectedRulesetId: string | null | undefined,
@@ -9,3 +15,18 @@ export function resolveRuleSetSelectValue(
9
15
  if (!selectedRulesetId) return undefined
10
16
  return ruleSets.some((ruleSet) => ruleSet.id === selectedRulesetId) ? selectedRulesetId : undefined
11
17
  }
18
+
19
+ // Selects which member-level custom rules to delete for a ruleset transition.
20
+ // Switching schedules preserves the member's saved custom hours (#2325): only
21
+ // an explicit "Reset to schedule" discards them so the shared schedule applies.
22
+ export function selectCustomRuleIdsToDelete(
23
+ transition: RuleSetTransition,
24
+ rules: AvailabilityRuleRef[],
25
+ ): string[] {
26
+ if (transition === 'switch') return []
27
+ return Array.from(new Set(rules.map((rule) => rule.id)))
28
+ }
29
+
30
+ export function requiresResetConfirmation(rules: AvailabilityRuleRef[]): boolean {
31
+ return rules.length > 0
32
+ }