@open-mercato/core 0.4.2-canary-10c7a8bf2a → 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.
- package/dist/modules/auth/lib/setup-app.js +2 -0
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/catalog/analytics.js +27 -0
- package/dist/modules/catalog/analytics.js.map +7 -0
- package/dist/modules/customers/analytics.js +50 -0
- package/dist/modules/customers/analytics.js.map +7 -0
- package/dist/modules/dashboards/acl.js +2 -1
- package/dist/modules/dashboards/acl.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/route.js +187 -0
- package/dist/modules/dashboards/api/widgets/data/route.js.map +7 -0
- package/dist/modules/dashboards/cli.js +142 -1
- package/dist/modules/dashboards/cli.js.map +2 -2
- package/dist/modules/dashboards/di.js +11 -0
- package/dist/modules/dashboards/di.js.map +7 -0
- package/dist/modules/dashboards/lib/aggregations.js +162 -0
- package/dist/modules/dashboards/lib/aggregations.js.map +7 -0
- package/dist/modules/dashboards/lib/formatters.js +34 -0
- package/dist/modules/dashboards/lib/formatters.js.map +7 -0
- package/dist/modules/dashboards/seed/analytics.js +383 -0
- package/dist/modules/dashboards/seed/analytics.js.map +7 -0
- package/dist/modules/dashboards/services/analyticsRegistry.js +52 -0
- package/dist/modules/dashboards/services/analyticsRegistry.js.map +7 -0
- package/dist/modules/dashboards/services/widgetDataService.js +207 -0
- package/dist/modules/dashboards/services/widgetDataService.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/config.js +18 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +128 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/config.js +18 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +126 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/config.js +18 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +151 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/config.js +18 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +126 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +16 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +123 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/config.js +18 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +128 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/config.js +21 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +211 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/config.js +19 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +131 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/config.js +19 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +153 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/config.js +22 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +180 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.js.map +7 -0
- package/dist/modules/sales/analytics.js +67 -0
- package/dist/modules/sales/analytics.js.map +7 -0
- package/package.json +2 -2
- package/src/modules/auth/lib/setup-app.ts +2 -0
- package/src/modules/catalog/analytics.ts +24 -0
- package/src/modules/customers/analytics.ts +47 -0
- package/src/modules/dashboards/acl.ts +1 -0
- package/src/modules/dashboards/api/widgets/data/route.ts +221 -0
- package/src/modules/dashboards/cli.ts +164 -1
- package/src/modules/dashboards/di.ts +9 -0
- package/src/modules/dashboards/i18n/de.json +115 -1
- package/src/modules/dashboards/i18n/en.json +115 -1
- package/src/modules/dashboards/i18n/es.json +115 -1
- package/src/modules/dashboards/i18n/pl.json +115 -1
- package/src/modules/dashboards/lib/__tests__/aggregations.test.ts +327 -0
- package/src/modules/dashboards/lib/__tests__/formatters.test.ts +128 -0
- package/src/modules/dashboards/lib/aggregations.ts +225 -0
- package/src/modules/dashboards/lib/formatters.ts +36 -0
- package/src/modules/dashboards/seed/analytics.ts +405 -0
- package/src/modules/dashboards/services/analyticsRegistry.ts +79 -0
- package/src/modules/dashboards/services/widgetDataService.ts +329 -0
- package/src/modules/dashboards/widgets/dashboard/aov-kpi/config.ts +20 -0
- package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +135 -0
- package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/config.ts +20 -0
- package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +133 -0
- package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/orders-by-status/config.ts +20 -0
- package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +154 -0
- package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/orders-kpi/config.ts +20 -0
- package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +133 -0
- package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +17 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +137 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-kpi/config.ts +20 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +135 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/config.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +220 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/config.ts +21 -0
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +131 -0
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/top-customers/config.ts +21 -0
- package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +161 -0
- package/src/modules/dashboards/widgets/dashboard/top-customers/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/top-products/config.ts +27 -0
- package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +181 -0
- package/src/modules/dashboards/widgets/dashboard/top-products/widget.ts +24 -0
- 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
|
+
}
|