@open-mercato/core 0.4.2-canary-cf7d9b4116 → 0.4.2-canary-e6bf6a353e

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 (136) hide show
  1. package/dist/modules/auth/lib/setup-app.js +2 -0
  2. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  3. package/dist/modules/catalog/analytics.js +27 -0
  4. package/dist/modules/catalog/analytics.js.map +7 -0
  5. package/dist/modules/customers/analytics.js +50 -0
  6. package/dist/modules/customers/analytics.js.map +7 -0
  7. package/dist/modules/dashboards/acl.js +2 -1
  8. package/dist/modules/dashboards/acl.js.map +2 -2
  9. package/dist/modules/dashboards/api/widgets/data/route.js +187 -0
  10. package/dist/modules/dashboards/api/widgets/data/route.js.map +7 -0
  11. package/dist/modules/dashboards/cli.js +142 -1
  12. package/dist/modules/dashboards/cli.js.map +2 -2
  13. package/dist/modules/dashboards/di.js +11 -0
  14. package/dist/modules/dashboards/di.js.map +7 -0
  15. package/dist/modules/dashboards/lib/aggregations.js +162 -0
  16. package/dist/modules/dashboards/lib/aggregations.js.map +7 -0
  17. package/dist/modules/dashboards/lib/formatters.js +34 -0
  18. package/dist/modules/dashboards/lib/formatters.js.map +7 -0
  19. package/dist/modules/dashboards/seed/analytics.js +383 -0
  20. package/dist/modules/dashboards/seed/analytics.js.map +7 -0
  21. package/dist/modules/dashboards/services/analyticsRegistry.js +52 -0
  22. package/dist/modules/dashboards/services/analyticsRegistry.js.map +7 -0
  23. package/dist/modules/dashboards/services/widgetDataService.js +207 -0
  24. package/dist/modules/dashboards/services/widgetDataService.js.map +7 -0
  25. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/config.js +18 -0
  26. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/config.js.map +7 -0
  27. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +128 -0
  28. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +7 -0
  29. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.js +25 -0
  30. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.js.map +7 -0
  31. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/config.js +18 -0
  32. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/config.js.map +7 -0
  33. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +126 -0
  34. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +7 -0
  35. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.js +25 -0
  36. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.js.map +7 -0
  37. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/config.js +18 -0
  38. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/config.js.map +7 -0
  39. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +151 -0
  40. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +7 -0
  41. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.js +25 -0
  42. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.js.map +7 -0
  43. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/config.js +18 -0
  44. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/config.js.map +7 -0
  45. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +126 -0
  46. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +7 -0
  47. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.js +25 -0
  48. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.js.map +7 -0
  49. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +16 -0
  50. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +7 -0
  51. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +123 -0
  52. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +7 -0
  53. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +25 -0
  54. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +7 -0
  55. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/config.js +18 -0
  56. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/config.js.map +7 -0
  57. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +128 -0
  58. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +7 -0
  59. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.js +25 -0
  60. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.js.map +7 -0
  61. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/config.js +21 -0
  62. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/config.js.map +7 -0
  63. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +211 -0
  64. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +7 -0
  65. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.js +25 -0
  66. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.js.map +7 -0
  67. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/config.js +19 -0
  68. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/config.js.map +7 -0
  69. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +131 -0
  70. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +7 -0
  71. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.js +25 -0
  72. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.js.map +7 -0
  73. package/dist/modules/dashboards/widgets/dashboard/top-customers/config.js +19 -0
  74. package/dist/modules/dashboards/widgets/dashboard/top-customers/config.js.map +7 -0
  75. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +153 -0
  76. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +7 -0
  77. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.js +25 -0
  78. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.js.map +7 -0
  79. package/dist/modules/dashboards/widgets/dashboard/top-products/config.js +22 -0
  80. package/dist/modules/dashboards/widgets/dashboard/top-products/config.js.map +7 -0
  81. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +180 -0
  82. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +7 -0
  83. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.js +25 -0
  84. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.js.map +7 -0
  85. package/dist/modules/sales/analytics.js +67 -0
  86. package/dist/modules/sales/analytics.js.map +7 -0
  87. package/package.json +2 -2
  88. package/src/modules/auth/lib/setup-app.ts +2 -0
  89. package/src/modules/catalog/analytics.ts +24 -0
  90. package/src/modules/customers/analytics.ts +47 -0
  91. package/src/modules/dashboards/acl.ts +1 -0
  92. package/src/modules/dashboards/api/widgets/data/route.ts +221 -0
  93. package/src/modules/dashboards/cli.ts +164 -1
  94. package/src/modules/dashboards/di.ts +9 -0
  95. package/src/modules/dashboards/i18n/de.json +115 -1
  96. package/src/modules/dashboards/i18n/en.json +115 -1
  97. package/src/modules/dashboards/i18n/es.json +115 -1
  98. package/src/modules/dashboards/i18n/pl.json +115 -1
  99. package/src/modules/dashboards/lib/__tests__/aggregations.test.ts +327 -0
  100. package/src/modules/dashboards/lib/__tests__/formatters.test.ts +128 -0
  101. package/src/modules/dashboards/lib/aggregations.ts +225 -0
  102. package/src/modules/dashboards/lib/formatters.ts +36 -0
  103. package/src/modules/dashboards/seed/analytics.ts +405 -0
  104. package/src/modules/dashboards/services/analyticsRegistry.ts +79 -0
  105. package/src/modules/dashboards/services/widgetDataService.ts +329 -0
  106. package/src/modules/dashboards/widgets/dashboard/aov-kpi/config.ts +20 -0
  107. package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +135 -0
  108. package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.ts +24 -0
  109. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/config.ts +20 -0
  110. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +133 -0
  111. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.ts +24 -0
  112. package/src/modules/dashboards/widgets/dashboard/orders-by-status/config.ts +20 -0
  113. package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +154 -0
  114. package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.ts +24 -0
  115. package/src/modules/dashboards/widgets/dashboard/orders-kpi/config.ts +20 -0
  116. package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +133 -0
  117. package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.ts +24 -0
  118. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +17 -0
  119. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +137 -0
  120. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +24 -0
  121. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/config.ts +20 -0
  122. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +135 -0
  123. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.ts +24 -0
  124. package/src/modules/dashboards/widgets/dashboard/revenue-trend/config.ts +24 -0
  125. package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +220 -0
  126. package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.ts +24 -0
  127. package/src/modules/dashboards/widgets/dashboard/sales-by-region/config.ts +21 -0
  128. package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +131 -0
  129. package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.ts +24 -0
  130. package/src/modules/dashboards/widgets/dashboard/top-customers/config.ts +21 -0
  131. package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +161 -0
  132. package/src/modules/dashboards/widgets/dashboard/top-customers/widget.ts +24 -0
  133. package/src/modules/dashboards/widgets/dashboard/top-products/config.ts +27 -0
  134. package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +181 -0
  135. package/src/modules/dashboards/widgets/dashboard/top-products/widget.ts +24 -0
  136. package/src/modules/sales/analytics.ts +64 -0
@@ -0,0 +1,20 @@
1
+ import { type DateRangePreset, isValidDateRangePreset } from '@open-mercato/ui/backend/date-range'
2
+
3
+ export type OrdersByStatusSettings = {
4
+ dateRange: DateRangePreset
5
+ variant: 'pie' | 'donut'
6
+ }
7
+
8
+ export const DEFAULT_SETTINGS: OrdersByStatusSettings = {
9
+ dateRange: 'this_month',
10
+ variant: 'donut',
11
+ }
12
+
13
+ export function hydrateSettings(raw: unknown): OrdersByStatusSettings {
14
+ if (!raw || typeof raw !== 'object') return { ...DEFAULT_SETTINGS }
15
+ const obj = raw as Record<string, unknown>
16
+ return {
17
+ dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,
18
+ variant: obj.variant === 'pie' || obj.variant === 'donut' ? obj.variant : DEFAULT_SETTINGS.variant,
19
+ }
20
+ }
@@ -0,0 +1,154 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
5
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
7
+ import { PieChart, type PieChartDataItem } from '@open-mercato/ui/backend/charts'
8
+ import {
9
+ DateRangeSelect,
10
+ InlineDateRangeSelect,
11
+ type DateRangePreset,
12
+ } from '@open-mercato/ui/backend/date-range'
13
+ import { DEFAULT_SETTINGS, hydrateSettings, type OrdersByStatusSettings } from './config'
14
+ import type { WidgetDataResponse } from '../../../services/widgetDataService'
15
+
16
+ async function fetchOrdersByStatusData(settings: OrdersByStatusSettings): Promise<WidgetDataResponse> {
17
+ const body = {
18
+ entityType: 'sales:orders',
19
+ metric: {
20
+ field: 'id',
21
+ aggregate: 'count',
22
+ },
23
+ groupBy: {
24
+ field: 'status',
25
+ },
26
+ dateRange: {
27
+ field: 'placedAt',
28
+ preset: settings.dateRange,
29
+ },
30
+ }
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 orders by status data')
41
+ }
42
+
43
+ return call.result as WidgetDataResponse
44
+ }
45
+
46
+ const ORDER_STATUS_KEYS: Record<string, string> = {
47
+ draft: 'dashboards.analytics.orderStatus.draft',
48
+ pending: 'dashboards.analytics.orderStatus.pending',
49
+ confirmed: 'dashboards.analytics.orderStatus.confirmed',
50
+ processing: 'dashboards.analytics.orderStatus.processing',
51
+ shipped: 'dashboards.analytics.orderStatus.shipped',
52
+ delivered: 'dashboards.analytics.orderStatus.delivered',
53
+ cancelled: 'dashboards.analytics.orderStatus.cancelled',
54
+ }
55
+
56
+ function formatStatusLabel(status: string | null, t: (key: string, fallback: string) => string): string {
57
+ if (!status) return t('dashboards.analytics.labels.unknown', 'Unknown')
58
+ const key = ORDER_STATUS_KEYS[status.toLowerCase()]
59
+ if (key) {
60
+ return t(key, status.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()))
61
+ }
62
+ return status.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())
63
+ }
64
+
65
+ const OrdersByStatusWidget: React.FC<DashboardWidgetComponentProps<OrdersByStatusSettings>> = ({
66
+ mode,
67
+ settings = DEFAULT_SETTINGS,
68
+ onSettingsChange,
69
+ refreshToken,
70
+ onRefreshStateChange,
71
+ }) => {
72
+ const t = useT()
73
+ const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])
74
+ const [data, setData] = React.useState<PieChartDataItem[]>([])
75
+ const [loading, setLoading] = React.useState(true)
76
+ const [error, setError] = React.useState<string | null>(null)
77
+
78
+ const refresh = React.useCallback(async () => {
79
+ onRefreshStateChange?.(true)
80
+ setLoading(true)
81
+ setError(null)
82
+ try {
83
+ const result = await fetchOrdersByStatusData(hydrated)
84
+ const chartData = result.data.map((item) => ({
85
+ name: formatStatusLabel(item.groupKey as string | null, t),
86
+ value: item.value ?? 0,
87
+ }))
88
+ setData(chartData)
89
+ } catch (err) {
90
+ console.error('Failed to load orders by status data', err)
91
+ setError(t('dashboards.analytics.widgets.ordersByStatus.error', 'Failed to load data'))
92
+ } finally {
93
+ setLoading(false)
94
+ onRefreshStateChange?.(false)
95
+ }
96
+ }, [hydrated, onRefreshStateChange, t])
97
+
98
+ React.useEffect(() => {
99
+ refresh().catch(() => {})
100
+ }, [refresh, refreshToken])
101
+
102
+ if (mode === 'settings') {
103
+ return (
104
+ <div className="space-y-4 text-sm">
105
+ <DateRangeSelect
106
+ id="orders-by-status-date-range"
107
+ label={t('dashboards.analytics.settings.dateRange', 'Date Range')}
108
+ value={hydrated.dateRange}
109
+ onChange={(dateRange: DateRangePreset) => onSettingsChange({ ...hydrated, dateRange })}
110
+ />
111
+ <div className="space-y-1.5">
112
+ <label
113
+ htmlFor="orders-by-status-variant"
114
+ className="text-xs font-semibold uppercase text-muted-foreground"
115
+ >
116
+ {t('dashboards.analytics.settings.chartVariant', 'Chart Style')}
117
+ </label>
118
+ <select
119
+ id="orders-by-status-variant"
120
+ className="w-full rounded-md border bg-background px-2 py-1 text-sm text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
121
+ value={hydrated.variant}
122
+ onChange={(e) => onSettingsChange({ ...hydrated, variant: e.target.value as 'pie' | 'donut' })}
123
+ >
124
+ <option value="donut">{t('dashboards.analytics.chartVariant.donut', 'Donut')}</option>
125
+ <option value="pie">{t('dashboards.analytics.chartVariant.pie', 'Pie')}</option>
126
+ </select>
127
+ </div>
128
+ </div>
129
+ )
130
+ }
131
+
132
+ return (
133
+ <div className="flex flex-col h-full">
134
+ <div className="flex justify-end mb-2">
135
+ <InlineDateRangeSelect
136
+ value={hydrated.dateRange}
137
+ onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange })}
138
+ />
139
+ </div>
140
+ <div className="flex-1 min-h-0">
141
+ <PieChart
142
+ data={data}
143
+ loading={loading}
144
+ error={error}
145
+ variant={hydrated.variant}
146
+ colors={['blue', 'emerald', 'amber', 'rose', 'violet', 'cyan']}
147
+ emptyMessage={t('dashboards.analytics.widgets.ordersByStatus.empty', 'No orders for this period')}
148
+ />
149
+ </div>
150
+ </div>
151
+ )
152
+ }
153
+
154
+ export default OrdersByStatusWidget
@@ -0,0 +1,24 @@
1
+ import type { DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'
2
+ import OrdersByStatusWidget from './widget.client'
3
+ import { DEFAULT_SETTINGS, hydrateSettings, type OrdersByStatusSettings } from './config'
4
+
5
+ const widget: DashboardWidgetModule<OrdersByStatusSettings> = {
6
+ metadata: {
7
+ id: 'dashboards.analytics.ordersByStatus',
8
+ title: 'Orders by Status',
9
+ description: 'Distribution of orders by status',
10
+ features: ['analytics.view', 'sales.orders.view'],
11
+ defaultSize: 'sm',
12
+ defaultEnabled: false,
13
+ defaultSettings: DEFAULT_SETTINGS,
14
+ tags: ['analytics', 'sales', 'chart'],
15
+ category: 'analytics',
16
+ icon: 'pie-chart',
17
+ supportsRefresh: true,
18
+ },
19
+ Widget: OrdersByStatusWidget,
20
+ hydrateSettings,
21
+ dehydrateSettings: (s) => ({ dateRange: s.dateRange, variant: s.variant }),
22
+ }
23
+
24
+ export default widget
@@ -0,0 +1,20 @@
1
+ import { type DateRangePreset, isValidDateRangePreset } from '@open-mercato/ui/backend/date-range'
2
+
3
+ export type OrdersKpiSettings = {
4
+ dateRange: DateRangePreset
5
+ showComparison: boolean
6
+ }
7
+
8
+ export const DEFAULT_SETTINGS: OrdersKpiSettings = {
9
+ dateRange: 'this_month',
10
+ showComparison: true,
11
+ }
12
+
13
+ export function hydrateSettings(raw: unknown): OrdersKpiSettings {
14
+ if (!raw || typeof raw !== 'object') return { ...DEFAULT_SETTINGS }
15
+ const obj = raw as Record<string, unknown>
16
+ return {
17
+ dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,
18
+ showComparison: typeof obj.showComparison === 'boolean' ? obj.showComparison : DEFAULT_SETTINGS.showComparison,
19
+ }
20
+ }
@@ -0,0 +1,133 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
5
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
7
+ import { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts'
8
+ import {
9
+ DateRangeSelect,
10
+ InlineDateRangeSelect,
11
+ type DateRangePreset,
12
+ getComparisonLabelKey,
13
+ } from '@open-mercato/ui/backend/date-range'
14
+ import { DEFAULT_SETTINGS, hydrateSettings, type OrdersKpiSettings } from './config'
15
+ import type { WidgetDataResponse } from '../../../services/widgetDataService'
16
+
17
+ async function fetchOrdersData(settings: OrdersKpiSettings): Promise<WidgetDataResponse> {
18
+ const body = {
19
+ entityType: 'sales:orders',
20
+ metric: {
21
+ field: 'id',
22
+ aggregate: 'count',
23
+ },
24
+ dateRange: {
25
+ field: 'placedAt',
26
+ preset: settings.dateRange,
27
+ },
28
+ comparison: settings.showComparison ? { type: 'previous_period' } : undefined,
29
+ }
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
43
+ }
44
+
45
+ const OrdersKpiWidget: React.FC<DashboardWidgetComponentProps<OrdersKpiSettings>> = ({
46
+ mode,
47
+ settings = DEFAULT_SETTINGS,
48
+ onSettingsChange,
49
+ refreshToken,
50
+ onRefreshStateChange,
51
+ }) => {
52
+ const t = useT()
53
+ const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])
54
+ const [value, setValue] = React.useState<number | null>(null)
55
+ const [trend, setTrend] = React.useState<KpiTrend | undefined>(undefined)
56
+ const [loading, setLoading] = React.useState(true)
57
+ const [error, setError] = React.useState<string | null>(null)
58
+
59
+ const refresh = React.useCallback(async () => {
60
+ onRefreshStateChange?.(true)
61
+ setLoading(true)
62
+ setError(null)
63
+ try {
64
+ const data = await fetchOrdersData(hydrated)
65
+ setValue(data.value)
66
+ if (data.comparison) {
67
+ setTrend({
68
+ value: data.comparison.change,
69
+ direction: data.comparison.direction,
70
+ })
71
+ } else {
72
+ setTrend(undefined)
73
+ }
74
+ } catch (err) {
75
+ console.error('Failed to load orders KPI data', err)
76
+ setError(t('dashboards.analytics.widgets.ordersKpi.error', 'Failed to load data'))
77
+ } finally {
78
+ setLoading(false)
79
+ onRefreshStateChange?.(false)
80
+ }
81
+ }, [hydrated, onRefreshStateChange, t])
82
+
83
+ React.useEffect(() => {
84
+ refresh().catch(() => {})
85
+ }, [refresh, refreshToken])
86
+
87
+ if (mode === 'settings') {
88
+ return (
89
+ <div className="space-y-4 text-sm">
90
+ <DateRangeSelect
91
+ id="orders-kpi-date-range"
92
+ label={t('dashboards.analytics.settings.dateRange', 'Date Range')}
93
+ value={hydrated.dateRange}
94
+ onChange={(dateRange: DateRangePreset) => onSettingsChange({ ...hydrated, dateRange })}
95
+ />
96
+ <div className="space-y-1.5">
97
+ <label className="flex items-center gap-2 text-sm">
98
+ <input
99
+ type="checkbox"
100
+ checked={hydrated.showComparison}
101
+ onChange={(e) => onSettingsChange({ ...hydrated, showComparison: e.target.checked })}
102
+ className="h-4 w-4 rounded border focus:ring-primary"
103
+ />
104
+ {t('dashboards.analytics.settings.showComparison', 'Show comparison')}
105
+ </label>
106
+ </div>
107
+ </div>
108
+ )
109
+ }
110
+
111
+ const comparisonLabelInfo = getComparisonLabelKey(hydrated.dateRange)
112
+ const comparisonLabel = hydrated.showComparison
113
+ ? t(comparisonLabelInfo.key, comparisonLabelInfo.fallback)
114
+ : undefined
115
+
116
+ return (
117
+ <KpiCard
118
+ value={value}
119
+ trend={trend}
120
+ comparisonLabel={comparisonLabel}
121
+ loading={loading}
122
+ error={error}
123
+ headerAction={
124
+ <InlineDateRangeSelect
125
+ value={hydrated.dateRange}
126
+ onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange })}
127
+ />
128
+ }
129
+ />
130
+ )
131
+ }
132
+
133
+ export default OrdersKpiWidget
@@ -0,0 +1,24 @@
1
+ import type { DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'
2
+ import OrdersKpiWidget from './widget.client'
3
+ import { DEFAULT_SETTINGS, hydrateSettings, type OrdersKpiSettings } from './config'
4
+
5
+ const widget: DashboardWidgetModule<OrdersKpiSettings> = {
6
+ metadata: {
7
+ id: 'dashboards.analytics.ordersKpi',
8
+ title: 'Orders',
9
+ description: 'Total order count with period comparison',
10
+ features: ['analytics.view', 'sales.orders.view'],
11
+ defaultSize: 'sm',
12
+ defaultEnabled: false,
13
+ defaultSettings: DEFAULT_SETTINGS,
14
+ tags: ['analytics', 'sales', 'kpi'],
15
+ category: 'analytics',
16
+ icon: 'shopping-cart',
17
+ supportsRefresh: true,
18
+ },
19
+ Widget: OrdersKpiWidget,
20
+ hydrateSettings,
21
+ dehydrateSettings: (s) => ({ dateRange: s.dateRange, showComparison: s.showComparison }),
22
+ }
23
+
24
+ export default widget
@@ -0,0 +1,17 @@
1
+ import { type DateRangePreset, isValidDateRangePreset } from '@open-mercato/ui/backend/date-range'
2
+
3
+ export type PipelineSummarySettings = {
4
+ dateRange: DateRangePreset
5
+ }
6
+
7
+ export const DEFAULT_SETTINGS: PipelineSummarySettings = {
8
+ dateRange: 'this_month',
9
+ }
10
+
11
+ export function hydrateSettings(raw: unknown): PipelineSummarySettings {
12
+ if (!raw || typeof raw !== 'object') return { ...DEFAULT_SETTINGS }
13
+ const obj = raw as Record<string, unknown>
14
+ return {
15
+ dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,
16
+ }
17
+ }
@@ -0,0 +1,137 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
5
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
7
+ import { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'
8
+ import {
9
+ DateRangeSelect,
10
+ InlineDateRangeSelect,
11
+ type DateRangePreset,
12
+ } from '@open-mercato/ui/backend/date-range'
13
+ import { DEFAULT_SETTINGS, hydrateSettings, type PipelineSummarySettings } from './config'
14
+ import type { WidgetDataResponse } from '../../../services/widgetDataService'
15
+ import { formatCurrencyCompact } from '../../../lib/formatters'
16
+
17
+ async function fetchPipelineData(settings: PipelineSummarySettings): Promise<WidgetDataResponse> {
18
+ const body = {
19
+ entityType: 'customers:deals',
20
+ metric: {
21
+ field: 'valueAmount',
22
+ aggregate: 'sum',
23
+ },
24
+ groupBy: {
25
+ field: 'pipelineStage',
26
+ resolveLabels: true,
27
+ },
28
+ dateRange: {
29
+ field: 'createdAt',
30
+ preset: settings.dateRange,
31
+ },
32
+ }
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
46
+ }
47
+
48
+ function formatStageLabel(stage: unknown, t: (key: string, fallback: string) => string): string {
49
+ if (stage == null || stage === '') return t('dashboards.analytics.labels.unknown', 'Unknown')
50
+ const stageStr = String(stage)
51
+ if (stageStr === '0' || stageStr === 'null' || stageStr === 'undefined') {
52
+ return t('dashboards.analytics.labels.unknown', 'Unknown')
53
+ }
54
+ return stageStr
55
+ .replace(/_/g, ' ')
56
+ .replace(/\b\w/g, (l) => l.toUpperCase())
57
+ }
58
+
59
+ const PipelineSummaryWidget: React.FC<DashboardWidgetComponentProps<PipelineSummarySettings>> = ({
60
+ mode,
61
+ settings = DEFAULT_SETTINGS,
62
+ onSettingsChange,
63
+ refreshToken,
64
+ onRefreshStateChange,
65
+ }) => {
66
+ const t = useT()
67
+ const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])
68
+ const [data, setData] = React.useState<BarChartDataItem[]>([])
69
+ const [loading, setLoading] = React.useState(true)
70
+ const [error, setError] = React.useState<string | null>(null)
71
+
72
+ const refresh = React.useCallback(async () => {
73
+ onRefreshStateChange?.(true)
74
+ setLoading(true)
75
+ setError(null)
76
+ try {
77
+ const result = await fetchPipelineData(hydrated)
78
+ const chartData = result.data
79
+ .filter((item) => item.groupKey != null && item.groupKey !== '' && String(item.groupKey) !== '0')
80
+ .map((item) => ({
81
+ stage: formatStageLabel(item.groupLabel ?? item.groupKey, t),
82
+ Value: item.value ?? 0,
83
+ }))
84
+ setData(chartData)
85
+ } catch (err) {
86
+ console.error('Failed to load pipeline data', err)
87
+ setError(t('dashboards.analytics.widgets.pipelineSummary.error', 'Failed to load data'))
88
+ } finally {
89
+ setLoading(false)
90
+ onRefreshStateChange?.(false)
91
+ }
92
+ }, [hydrated, onRefreshStateChange, t])
93
+
94
+ React.useEffect(() => {
95
+ refresh().catch(() => {})
96
+ }, [refresh, refreshToken])
97
+
98
+ if (mode === 'settings') {
99
+ return (
100
+ <div className="space-y-4 text-sm">
101
+ <DateRangeSelect
102
+ id="pipeline-summary-date-range"
103
+ label={t('dashboards.analytics.settings.dateRange', 'Date Range')}
104
+ value={hydrated.dateRange}
105
+ onChange={(dateRange: DateRangePreset) => onSettingsChange({ ...hydrated, dateRange })}
106
+ />
107
+ </div>
108
+ )
109
+ }
110
+
111
+ return (
112
+ <div className="flex flex-col h-full">
113
+ <div className="flex justify-end mb-2">
114
+ <InlineDateRangeSelect
115
+ value={hydrated.dateRange}
116
+ onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange })}
117
+ />
118
+ </div>
119
+ <div className="flex-1 min-h-0">
120
+ <BarChart
121
+ data={data}
122
+ index="stage"
123
+ categories={['Value']}
124
+ categoryLabels={{ Value: t('dashboards.analytics.labels.value', 'Value') }}
125
+ loading={loading}
126
+ error={error}
127
+ valueFormatter={formatCurrencyCompact}
128
+ colors={['violet']}
129
+ showLegend={false}
130
+ emptyMessage={t('dashboards.analytics.widgets.pipelineSummary.empty', 'No deal data for this period')}
131
+ />
132
+ </div>
133
+ </div>
134
+ )
135
+ }
136
+
137
+ export default PipelineSummaryWidget
@@ -0,0 +1,24 @@
1
+ import type { DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'
2
+ import PipelineSummaryWidget from './widget.client'
3
+ import { DEFAULT_SETTINGS, hydrateSettings, type PipelineSummarySettings } from './config'
4
+
5
+ const widget: DashboardWidgetModule<PipelineSummarySettings> = {
6
+ metadata: {
7
+ id: 'dashboards.analytics.pipelineSummary',
8
+ title: 'Pipeline Summary',
9
+ description: 'Deal value by pipeline stage',
10
+ features: ['analytics.view', 'customers.deals.view'],
11
+ defaultSize: 'md',
12
+ defaultEnabled: false,
13
+ defaultSettings: DEFAULT_SETTINGS,
14
+ tags: ['analytics', 'customers', 'deals', 'chart'],
15
+ category: 'analytics',
16
+ icon: 'git-branch',
17
+ supportsRefresh: true,
18
+ },
19
+ Widget: PipelineSummaryWidget,
20
+ hydrateSettings,
21
+ dehydrateSettings: (s) => ({ dateRange: s.dateRange }),
22
+ }
23
+
24
+ export default widget
@@ -0,0 +1,20 @@
1
+ import { type DateRangePreset, isValidDateRangePreset } from '@open-mercato/ui/backend/date-range'
2
+
3
+ export type RevenueKpiSettings = {
4
+ dateRange: DateRangePreset
5
+ showComparison: boolean
6
+ }
7
+
8
+ export const DEFAULT_SETTINGS: RevenueKpiSettings = {
9
+ dateRange: 'this_month',
10
+ showComparison: true,
11
+ }
12
+
13
+ export function hydrateSettings(raw: unknown): RevenueKpiSettings {
14
+ if (!raw || typeof raw !== 'object') return { ...DEFAULT_SETTINGS }
15
+ const obj = raw as Record<string, unknown>
16
+ return {
17
+ dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,
18
+ showComparison: typeof obj.showComparison === 'boolean' ? obj.showComparison : DEFAULT_SETTINGS.showComparison,
19
+ }
20
+ }