medusa-plugin-statistics 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.medusa/server/medusa-config.d.ts +1 -0
  2. package/.medusa/server/medusa-config.js +23 -0
  3. package/.medusa/server/src/admin/index.js +790 -0
  4. package/.medusa/server/src/admin/index.mjs +789 -0
  5. package/.medusa/server/src/api/admin/mcp/route.d.ts +4 -0
  6. package/.medusa/server/src/api/admin/mcp/route.js +60 -0
  7. package/.medusa/server/src/api/admin/statistics/layout/route.d.ts +4 -0
  8. package/.medusa/server/src/api/admin/statistics/layout/route.js +37 -0
  9. package/.medusa/server/src/api/admin/statistics/low-stock/route.d.ts +3 -0
  10. package/.medusa/server/src/api/admin/statistics/low-stock/route.js +85 -0
  11. package/.medusa/server/src/api/admin/statistics/recalculate/route.d.ts +3 -0
  12. package/.medusa/server/src/api/admin/statistics/recalculate/route.js +32 -0
  13. package/.medusa/server/src/api/admin/statistics/recent-orders/route.d.ts +3 -0
  14. package/.medusa/server/src/api/admin/statistics/recent-orders/route.js +19 -0
  15. package/.medusa/server/src/api/admin/statistics/route.d.ts +3 -0
  16. package/.medusa/server/src/api/admin/statistics/route.js +55 -0
  17. package/.medusa/server/src/api/middlewares.d.ts +2 -0
  18. package/.medusa/server/src/api/middlewares.js +44 -0
  19. package/.medusa/server/src/api/validators.d.ts +85 -0
  20. package/.medusa/server/src/api/validators.js +30 -0
  21. package/.medusa/server/src/jobs/update-statistics.d.ts +7 -0
  22. package/.medusa/server/src/jobs/update-statistics.js +165 -0
  23. package/.medusa/server/src/mcp/server.d.ts +3 -0
  24. package/.medusa/server/src/mcp/server.js +23 -0
  25. package/.medusa/server/src/mcp/tools/customers.d.ts +3 -0
  26. package/.medusa/server/src/mcp/tools/customers.js +72 -0
  27. package/.medusa/server/src/mcp/tools/inventory.d.ts +3 -0
  28. package/.medusa/server/src/mcp/tools/inventory.js +70 -0
  29. package/.medusa/server/src/mcp/tools/orders.d.ts +3 -0
  30. package/.medusa/server/src/mcp/tools/orders.js +80 -0
  31. package/.medusa/server/src/mcp/tools/products.d.ts +3 -0
  32. package/.medusa/server/src/mcp/tools/products.js +72 -0
  33. package/.medusa/server/src/mcp/tools/query.d.ts +3 -0
  34. package/.medusa/server/src/mcp/tools/query.js +42 -0
  35. package/.medusa/server/src/modules/statistics/index.d.ts +22 -0
  36. package/.medusa/server/src/modules/statistics/index.js +25 -0
  37. package/.medusa/server/src/modules/statistics/migrations/Migration20260409171831.d.ts +5 -0
  38. package/.medusa/server/src/modules/statistics/migrations/Migration20260409171831.js +15 -0
  39. package/.medusa/server/src/modules/statistics/models/statistics-daily.d.ts +13 -0
  40. package/.medusa/server/src/modules/statistics/models/statistics-daily.js +18 -0
  41. package/.medusa/server/src/modules/statistics/service.d.ts +18 -0
  42. package/.medusa/server/src/modules/statistics/service.js +9 -0
  43. package/LICENSE +21 -0
  44. package/package.json +97 -0
@@ -0,0 +1,789 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { useState, useRef, useEffect, useCallback, useMemo } from "react";
3
+ import { defineRouteConfig } from "@medusajs/admin-sdk";
4
+ import { XCircle, ExclamationCircle, ChatBubbleLeftRight, ChartBar } from "@medusajs/icons";
5
+ import { Container, Heading, Text, Badge, Drawer, Button, Select, Input, CodeBlock, toast } from "@medusajs/ui";
6
+ import { useContainerWidth, ResponsiveGridLayout, verticalCompactor } from "react-grid-layout";
7
+ import "react-grid-layout/css/styles.css";
8
+ import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query";
9
+ import Medusa from "@medusajs/js-sdk";
10
+ import { ResponsiveContainer, AreaChart, XAxis, YAxis, Tooltip, Area, BarChart, Bar, LineChart, Line, PieChart, Pie, Cell } from "recharts";
11
+ import { Link } from "react-router-dom";
12
+ const sdk = new Medusa({
13
+ baseUrl: "/",
14
+ debug: false,
15
+ auth: {
16
+ type: "session"
17
+ }
18
+ });
19
+ const useStatistics = (period) => {
20
+ return useQuery({
21
+ queryFn: () => sdk.client.fetch("/admin/statistics", { query: { period } }),
22
+ queryKey: ["statistics", period]
23
+ });
24
+ };
25
+ const useRecentOrders = (limit = 10) => {
26
+ return useQuery({
27
+ queryFn: () => sdk.client.fetch("/admin/statistics/recent-orders", { query: { limit } }),
28
+ queryKey: ["statistics-recent-orders", limit]
29
+ });
30
+ };
31
+ const useLowStock = (threshold = 10) => {
32
+ return useQuery({
33
+ queryFn: () => sdk.client.fetch("/admin/statistics/low-stock", { query: { threshold } }),
34
+ queryKey: ["statistics-low-stock", threshold]
35
+ });
36
+ };
37
+ const useStatisticsLayout = () => {
38
+ return useQuery({
39
+ queryFn: () => sdk.client.fetch("/admin/statistics/layout"),
40
+ queryKey: ["statistics-layout"]
41
+ });
42
+ };
43
+ const useSaveStatisticsLayout = () => {
44
+ const queryClient = useQueryClient();
45
+ return useMutation({
46
+ mutationFn: (layout) => sdk.client.fetch("/admin/statistics/layout", { method: "POST", body: { layout } }),
47
+ onSuccess: () => {
48
+ queryClient.invalidateQueries({ queryKey: ["statistics-layout"] });
49
+ }
50
+ });
51
+ };
52
+ const useRecalculateStatistics = () => {
53
+ const queryClient = useQueryClient();
54
+ return useMutation({
55
+ mutationFn: () => sdk.client.fetch("/admin/statistics/recalculate", { method: "POST", body: {} }),
56
+ onSuccess: () => {
57
+ queryClient.invalidateQueries({ queryKey: ["statistics"] });
58
+ }
59
+ });
60
+ };
61
+ const RevenueWidget = ({ statistics, totals }) => {
62
+ var _a;
63
+ const chartData = statistics.map((s) => ({
64
+ date: new Date(s.date).toLocaleDateString("en-US", { month: "short", day: "numeric" }),
65
+ revenue: s.revenue_total
66
+ }));
67
+ return /* @__PURE__ */ jsxs(Container, { className: "h-full p-4 flex flex-col", children: [
68
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
69
+ /* @__PURE__ */ jsx(Heading, { level: "h3", children: "Revenue" }),
70
+ /* @__PURE__ */ jsxs(Text, { size: "xlarge", weight: "plus", className: "text-ui-fg-base", children: [
71
+ "$",
72
+ ((_a = totals.revenue_total) == null ? void 0 : _a.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })) ?? "0.00"
73
+ ] })
74
+ ] }),
75
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: chartData.length > 0 ? /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", minWidth: 0, minHeight: 0, children: /* @__PURE__ */ jsxs(AreaChart, { data: chartData, children: [
76
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: "revGrad", x1: "0", y1: "0", x2: "0", y2: "1", children: [
77
+ /* @__PURE__ */ jsx("stop", { offset: "5%", stopColor: "#6366f1", stopOpacity: 0.3 }),
78
+ /* @__PURE__ */ jsx("stop", { offset: "95%", stopColor: "#6366f1", stopOpacity: 0 })
79
+ ] }) }),
80
+ /* @__PURE__ */ jsx(XAxis, { dataKey: "date", tick: { fontSize: 11 } }),
81
+ /* @__PURE__ */ jsx(YAxis, { tick: { fontSize: 11 }, width: 60 }),
82
+ /* @__PURE__ */ jsx(Tooltip, { formatter: (v) => [`$${v.toFixed(2)}`, "Revenue"] }),
83
+ /* @__PURE__ */ jsx(Area, { type: "monotone", dataKey: "revenue", stroke: "#6366f1", fill: "url(#revGrad)" })
84
+ ] }) }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted", children: "No data for this period." }) })
85
+ ] });
86
+ };
87
+ const OrdersCountWidget = ({ statistics, totals }) => {
88
+ var _a;
89
+ const chartData = statistics.map((s) => ({
90
+ date: new Date(s.date).toLocaleDateString("en-US", { month: "short", day: "numeric" }),
91
+ orders: s.order_count
92
+ }));
93
+ return /* @__PURE__ */ jsxs(Container, { className: "h-full p-4 flex flex-col", children: [
94
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
95
+ /* @__PURE__ */ jsx(Heading, { level: "h3", children: "Orders" }),
96
+ /* @__PURE__ */ jsx(Text, { size: "xlarge", weight: "plus", className: "text-ui-fg-base", children: ((_a = totals.order_count) == null ? void 0 : _a.toLocaleString()) ?? 0 })
97
+ ] }),
98
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: chartData.length > 0 ? /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", minWidth: 0, minHeight: 0, children: /* @__PURE__ */ jsxs(BarChart, { data: chartData, children: [
99
+ /* @__PURE__ */ jsx(XAxis, { dataKey: "date", tick: { fontSize: 11 } }),
100
+ /* @__PURE__ */ jsx(YAxis, { tick: { fontSize: 11 }, width: 40, allowDecimals: false }),
101
+ /* @__PURE__ */ jsx(Tooltip, {}),
102
+ /* @__PURE__ */ jsx(Bar, { dataKey: "orders", fill: "#10b981", radius: [4, 4, 0, 0] })
103
+ ] }) }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted", children: "No data for this period." }) })
104
+ ] });
105
+ };
106
+ const PERIOD_LABELS$1 = {
107
+ today: "Today",
108
+ week: "Last 7 days",
109
+ month: "Last 30 days"
110
+ };
111
+ const AovWidget = ({ statistics, totals, period }) => {
112
+ var _a;
113
+ const chartData = statistics.map((s) => ({
114
+ date: new Date(s.date).toLocaleDateString("en-US", { month: "short", day: "numeric" }),
115
+ aov: s.average_order_value
116
+ }));
117
+ return /* @__PURE__ */ jsxs(Container, { className: "h-full p-4 flex flex-col", children: [
118
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-1", children: [
119
+ /* @__PURE__ */ jsx(Heading, { level: "h3", children: "Avg Order Value" }),
120
+ /* @__PURE__ */ jsx(Text, { size: "xsmall", className: "text-ui-fg-muted", children: PERIOD_LABELS$1[period] ?? period })
121
+ ] }),
122
+ /* @__PURE__ */ jsxs(Text, { size: "xlarge", weight: "plus", className: "text-ui-fg-base", children: [
123
+ "$",
124
+ ((_a = totals.average_order_value) == null ? void 0 : _a.toFixed(2)) ?? "0.00"
125
+ ] }),
126
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0 mt-2", children: chartData.length > 1 ? /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", minWidth: 0, minHeight: 0, children: /* @__PURE__ */ jsxs(LineChart, { data: chartData, children: [
127
+ /* @__PURE__ */ jsx(XAxis, { dataKey: "date", tick: { fontSize: 11 } }),
128
+ /* @__PURE__ */ jsx(YAxis, { tick: { fontSize: 11 }, width: 50, tickFormatter: (v) => `$${v}` }),
129
+ /* @__PURE__ */ jsx(Tooltip, { formatter: (v) => [`$${v.toFixed(2)}`, "AOV"] }),
130
+ /* @__PURE__ */ jsx(
131
+ Line,
132
+ {
133
+ type: "monotone",
134
+ dataKey: "aov",
135
+ stroke: "#f59e0b",
136
+ strokeWidth: 2,
137
+ dot: false
138
+ }
139
+ )
140
+ ] }) }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted", children: "Not enough data to chart." }) })
141
+ ] });
142
+ };
143
+ const TopProductsWidget = ({ statistics }) => {
144
+ const aggregated = {};
145
+ for (const stat of statistics) {
146
+ for (const p of (stat == null ? void 0 : stat.top_products) ?? []) {
147
+ if (!(p == null ? void 0 : p.product_id)) continue;
148
+ const existing = aggregated[p.product_id];
149
+ if (existing) {
150
+ existing.quantity_sold += p.quantity_sold;
151
+ } else {
152
+ aggregated[p.product_id] = { title: p.title, quantity_sold: p.quantity_sold };
153
+ }
154
+ }
155
+ }
156
+ const products = Object.values(aggregated).sort((a, b) => b.quantity_sold - a.quantity_sold).slice(0, 8);
157
+ const chartData = products.map((p) => ({
158
+ name: p.title.length > 20 ? p.title.slice(0, 20) + "..." : p.title,
159
+ sold: p.quantity_sold
160
+ }));
161
+ return /* @__PURE__ */ jsxs(Container, { className: "h-full p-4 flex flex-col", children: [
162
+ /* @__PURE__ */ jsx(Heading, { level: "h3", className: "mb-2", children: "Top Products" }),
163
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: chartData.length > 0 ? /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", minWidth: 0, minHeight: 0, children: /* @__PURE__ */ jsxs(BarChart, { data: chartData, layout: "vertical", children: [
164
+ /* @__PURE__ */ jsx(XAxis, { type: "number", tick: { fontSize: 11 }, allowDecimals: false }),
165
+ /* @__PURE__ */ jsx(
166
+ YAxis,
167
+ {
168
+ type: "category",
169
+ dataKey: "name",
170
+ tick: { fontSize: 11 },
171
+ width: 120
172
+ }
173
+ ),
174
+ /* @__PURE__ */ jsx(Tooltip, {}),
175
+ /* @__PURE__ */ jsx(Bar, { dataKey: "sold", fill: "#8b5cf6", radius: [0, 4, 4, 0] })
176
+ ] }) }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted", children: "No data yet." }) })
177
+ ] });
178
+ };
179
+ const STATUS_COLORS = {
180
+ completed: "green",
181
+ pending: "orange",
182
+ canceled: "red",
183
+ archived: "grey",
184
+ requires_action: "blue"
185
+ };
186
+ const RecentOrdersWidget = (_props) => {
187
+ const { data, isLoading } = useRecentOrders(8);
188
+ const orders = (data == null ? void 0 : data.orders) ?? [];
189
+ return /* @__PURE__ */ jsxs(Container, { className: "h-full p-0 flex flex-col", children: [
190
+ /* @__PURE__ */ jsx("div", { className: "px-4 pt-4 pb-2", children: /* @__PURE__ */ jsx(Heading, { level: "h3", children: "Recent Orders" }) }),
191
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto", children: [
192
+ isLoading && /* @__PURE__ */ jsx(Text, { size: "small", className: "px-4 text-ui-fg-muted", children: "Loading..." }),
193
+ !isLoading && orders.length === 0 && /* @__PURE__ */ jsx(Text, { size: "small", className: "px-4 text-ui-fg-muted", children: "No orders yet." }),
194
+ orders.map((order) => {
195
+ var _a;
196
+ return /* @__PURE__ */ jsxs(
197
+ Link,
198
+ {
199
+ to: `/orders/${order.id}`,
200
+ className: "flex items-center justify-between px-4 py-2 border-b border-ui-border-base last:border-b-0 hover:bg-ui-bg-base-hover transition-colors no-underline",
201
+ children: [
202
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
203
+ /* @__PURE__ */ jsxs(Text, { size: "small", weight: "plus", children: [
204
+ "#",
205
+ order.display_id
206
+ ] }),
207
+ /* @__PURE__ */ jsx(Text, { size: "xsmall", className: "text-ui-fg-subtle", children: ((_a = order.customer) == null ? void 0 : _a.first_name) ? `${order.customer.first_name} ${order.customer.last_name ?? ""}` : order.email })
208
+ ] }),
209
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
210
+ /* @__PURE__ */ jsxs(Text, { size: "small", className: "font-mono", children: [
211
+ "$",
212
+ (order.total ?? 0).toFixed(2)
213
+ ] }),
214
+ /* @__PURE__ */ jsx(Badge, { size: "xsmall", color: STATUS_COLORS[order.status] ?? "grey", children: order.status })
215
+ ] })
216
+ ]
217
+ },
218
+ order.id
219
+ );
220
+ })
221
+ ] })
222
+ ] });
223
+ };
224
+ const COLORS = ["#6366f1", "#a5b4fc"];
225
+ const CustomerCountWidget = ({ totals }) => {
226
+ const newCount = totals.new_customer_count ?? 0;
227
+ const returningCount = totals.returning_customer_count ?? 0;
228
+ const total = newCount + returningCount;
229
+ const chartData = [
230
+ { name: "New", value: newCount },
231
+ { name: "Returning", value: returningCount }
232
+ ];
233
+ return /* @__PURE__ */ jsxs(Container, { className: "h-full p-4 flex flex-col", children: [
234
+ /* @__PURE__ */ jsx(Heading, { level: "h3", children: "Customers" }),
235
+ /* @__PURE__ */ jsx(Text, { size: "xlarge", weight: "plus", className: "text-ui-fg-base mt-1", children: total }),
236
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0 mt-1", children: total > 0 ? /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", minWidth: 0, minHeight: 0, children: /* @__PURE__ */ jsxs(PieChart, { children: [
237
+ /* @__PURE__ */ jsx(
238
+ Pie,
239
+ {
240
+ data: chartData,
241
+ dataKey: "value",
242
+ innerRadius: "50%",
243
+ outerRadius: "80%",
244
+ paddingAngle: 2,
245
+ children: chartData.map((_, i) => /* @__PURE__ */ jsx(Cell, { fill: COLORS[i] }, i))
246
+ }
247
+ ),
248
+ /* @__PURE__ */ jsx(Tooltip, {})
249
+ ] }) }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted", children: "No data yet." }) }),
250
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-4 text-xs text-ui-fg-subtle", children: [
251
+ /* @__PURE__ */ jsxs("span", { children: [
252
+ /* @__PURE__ */ jsx("span", { className: "inline-block w-2 h-2 rounded-full mr-1", style: { backgroundColor: COLORS[0] } }),
253
+ "New: ",
254
+ newCount
255
+ ] }),
256
+ /* @__PURE__ */ jsxs("span", { children: [
257
+ /* @__PURE__ */ jsx("span", { className: "inline-block w-2 h-2 rounded-full mr-1", style: { backgroundColor: COLORS[1] } }),
258
+ "Returning: ",
259
+ returningCount
260
+ ] })
261
+ ] })
262
+ ] });
263
+ };
264
+ const FulfillmentWidget = ({ totals }) => {
265
+ const count = totals.pending_fulfillment_count ?? 0;
266
+ const color = count === 0 ? "green" : count <= 5 ? "orange" : "red";
267
+ return /* @__PURE__ */ jsxs(Container, { className: "h-full p-4 flex flex-col", children: [
268
+ /* @__PURE__ */ jsx(Heading, { level: "h3", children: "Pending Fulfillment" }),
269
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col items-center justify-center", children: [
270
+ /* @__PURE__ */ jsx(Text, { size: "xlarge", weight: "plus", className: "text-ui-fg-base text-4xl", children: count }),
271
+ /* @__PURE__ */ jsx(Badge, { size: "small", color, className: "mt-2", children: count === 0 ? "All fulfilled" : `${count} awaiting` })
272
+ ] })
273
+ ] });
274
+ };
275
+ const LowStockWidget = (_props) => {
276
+ const { data, isLoading } = useLowStock(50);
277
+ const warnings = (data == null ? void 0 : data.warnings) ?? [];
278
+ return /* @__PURE__ */ jsxs(Container, { className: "h-full p-0 flex flex-col", children: [
279
+ /* @__PURE__ */ jsxs("div", { className: "px-4 pt-4 pb-2 flex items-center justify-between", children: [
280
+ /* @__PURE__ */ jsx(Heading, { level: "h3", children: "Inventory Warnings" }),
281
+ warnings.length > 0 && /* @__PURE__ */ jsx(Badge, { size: "xsmall", color: "red", children: warnings.length })
282
+ ] }),
283
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto", children: [
284
+ isLoading && /* @__PURE__ */ jsx(Text, { size: "small", className: "px-4 text-ui-fg-muted", children: "Loading..." }),
285
+ !isLoading && warnings.length === 0 && /* @__PURE__ */ jsx(Text, { size: "small", className: "px-4 text-ui-fg-muted", children: "All inventory levels OK." }),
286
+ warnings.map((w, i) => /* @__PURE__ */ jsxs(
287
+ Link,
288
+ {
289
+ to: `/inventory/${w.inventory_item_id}`,
290
+ className: "flex items-start gap-2 px-4 py-2 border-b border-ui-border-base last:border-b-0 hover:bg-ui-bg-base-hover transition-colors no-underline",
291
+ children: [
292
+ w.reason === "no_lots" ? /* @__PURE__ */ jsx(XCircle, { className: "text-ui-fg-error mt-0.5 shrink-0" }) : /* @__PURE__ */ jsx(ExclamationCircle, { className: "text-ui-tag-orange-icon mt-0.5 shrink-0" }),
293
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
294
+ /* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", className: "truncate", children: w.title || w.sku }),
295
+ /* @__PURE__ */ jsxs(Text, { size: "xsmall", className: "text-ui-fg-subtle", children: [
296
+ w.location_name,
297
+ w.reason === "no_lots" ? " — No stock lots" : ` — ${w.available_quantity} available`
298
+ ] })
299
+ ] })
300
+ ]
301
+ },
302
+ `${w.inventory_item_id}-${w.location_id}`
303
+ ))
304
+ ] })
305
+ ] });
306
+ };
307
+ const WIDGETS = [
308
+ {
309
+ id: "revenue",
310
+ title: "Revenue",
311
+ layouts: {
312
+ lg: { x: 0, y: 0, w: 4, h: 3, minW: 3, minH: 2 },
313
+ md: { x: 0, y: 0, w: 4, h: 3, minW: 3, minH: 2 },
314
+ sm: { x: 0, y: 0, w: 4, h: 3, minW: 2, minH: 2 }
315
+ },
316
+ component: RevenueWidget
317
+ },
318
+ {
319
+ id: "orders",
320
+ title: "Orders",
321
+ layouts: {
322
+ lg: { x: 4, y: 0, w: 4, h: 3, minW: 3, minH: 2 },
323
+ md: { x: 4, y: 0, w: 4, h: 3, minW: 3, minH: 2 },
324
+ sm: { x: 0, y: 3, w: 4, h: 3, minW: 2, minH: 2 }
325
+ },
326
+ component: OrdersCountWidget
327
+ },
328
+ {
329
+ id: "aov",
330
+ title: "Avg Order Value",
331
+ layouts: {
332
+ lg: { x: 8, y: 0, w: 4, h: 3, minW: 2, minH: 2 },
333
+ md: { x: 0, y: 3, w: 4, h: 3, minW: 2, minH: 2 },
334
+ sm: { x: 0, y: 6, w: 4, h: 3, minW: 2, minH: 2 }
335
+ },
336
+ component: AovWidget
337
+ },
338
+ {
339
+ id: "top-products",
340
+ title: "Top Products",
341
+ layouts: {
342
+ lg: { x: 0, y: 3, w: 4, h: 4, minW: 3, minH: 3 },
343
+ md: { x: 4, y: 3, w: 4, h: 3, minW: 3, minH: 3 },
344
+ sm: { x: 0, y: 9, w: 4, h: 4, minW: 2, minH: 3 }
345
+ },
346
+ component: TopProductsWidget
347
+ },
348
+ {
349
+ id: "recent-orders",
350
+ title: "Recent Orders",
351
+ layouts: {
352
+ lg: { x: 4, y: 3, w: 8, h: 4, minW: 4, minH: 3 },
353
+ md: { x: 0, y: 6, w: 8, h: 4, minW: 4, minH: 3 },
354
+ sm: { x: 0, y: 13, w: 4, h: 4, minW: 2, minH: 3 }
355
+ },
356
+ component: RecentOrdersWidget
357
+ },
358
+ {
359
+ id: "customers",
360
+ title: "Customers",
361
+ layouts: {
362
+ lg: { x: 0, y: 7, w: 3, h: 3, minW: 2, minH: 2 },
363
+ md: { x: 0, y: 10, w: 4, h: 3, minW: 2, minH: 2 },
364
+ sm: { x: 0, y: 17, w: 4, h: 3, minW: 2, minH: 2 }
365
+ },
366
+ component: CustomerCountWidget
367
+ },
368
+ {
369
+ id: "fulfillment",
370
+ title: "Pending Fulfillment",
371
+ layouts: {
372
+ lg: { x: 3, y: 7, w: 3, h: 3, minW: 2, minH: 2 },
373
+ md: { x: 4, y: 10, w: 4, h: 3, minW: 2, minH: 2 },
374
+ sm: { x: 0, y: 20, w: 4, h: 3, minW: 2, minH: 2 }
375
+ },
376
+ component: FulfillmentWidget
377
+ },
378
+ {
379
+ id: "low-stock",
380
+ title: "Low Stock",
381
+ layouts: {
382
+ lg: { x: 6, y: 7, w: 6, h: 3, minW: 4, minH: 2 },
383
+ md: { x: 0, y: 13, w: 8, h: 3, minW: 4, minH: 2 },
384
+ sm: { x: 0, y: 23, w: 4, h: 3, minW: 2, minH: 2 }
385
+ },
386
+ component: LowStockWidget
387
+ }
388
+ ];
389
+ const baseUrl = "";
390
+ let rpcId = 1;
391
+ let mcpSessionId = null;
392
+ async function mcpRequest(method, params) {
393
+ const headers = {
394
+ "Content-Type": "application/json",
395
+ Accept: "application/json, text/event-stream"
396
+ };
397
+ if (mcpSessionId) {
398
+ headers["mcp-session-id"] = mcpSessionId;
399
+ }
400
+ const res = await fetch(`${baseUrl}/admin/mcp`, {
401
+ method: "POST",
402
+ credentials: "include",
403
+ headers,
404
+ body: JSON.stringify({
405
+ jsonrpc: "2.0",
406
+ id: rpcId++,
407
+ method,
408
+ params
409
+ })
410
+ });
411
+ const newSessionId = res.headers.get("mcp-session-id");
412
+ if (newSessionId) {
413
+ mcpSessionId = newSessionId;
414
+ }
415
+ if (!res.ok) {
416
+ const text = await res.text();
417
+ throw new Error(`MCP error ${res.status}: ${text}`);
418
+ }
419
+ return res.json();
420
+ }
421
+ async function mcpInitialize() {
422
+ mcpSessionId = null;
423
+ return mcpRequest("initialize", {
424
+ protocolVersion: "2025-03-26",
425
+ capabilities: {},
426
+ clientInfo: { name: "medusa-admin-chat", version: "1.0.0" }
427
+ });
428
+ }
429
+ async function mcpListTools() {
430
+ var _a;
431
+ const res = await mcpRequest("tools/list", {});
432
+ return ((_a = res == null ? void 0 : res.result) == null ? void 0 : _a.tools) ?? (res == null ? void 0 : res.tools) ?? [];
433
+ }
434
+ async function mcpCallTool(name, args) {
435
+ var _a;
436
+ const res = await mcpRequest("tools/call", { name, arguments: args });
437
+ const content = ((_a = res == null ? void 0 : res.result) == null ? void 0 : _a.content) ?? (res == null ? void 0 : res.content) ?? [];
438
+ return content.map((c) => c.text ?? JSON.stringify(c)).join("\n");
439
+ }
440
+ const McpQuery = () => {
441
+ var _a, _b;
442
+ const [open, setOpen] = useState(false);
443
+ const [tools, setTools] = useState([]);
444
+ const [selectedToolName, setSelectedToolName] = useState("");
445
+ const [paramValues, setParamValues] = useState({});
446
+ const [messages, setMessages] = useState([]);
447
+ const [loading, setLoading] = useState(false);
448
+ const [initialized, setInitialized] = useState(false);
449
+ const messagesEndRef = useRef(null);
450
+ const selectedTool = tools.find((t) => t.name === selectedToolName) ?? null;
451
+ useEffect(() => {
452
+ var _a2;
453
+ (_a2 = messagesEndRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth" });
454
+ }, [messages]);
455
+ const handleOpen = async (isOpen) => {
456
+ setOpen(isOpen);
457
+ if (isOpen && !initialized) {
458
+ try {
459
+ await mcpInitialize();
460
+ const toolList = await mcpListTools();
461
+ setTools(toolList);
462
+ setInitialized(true);
463
+ } catch (err) {
464
+ console.error("[MCP] Connection error:", err);
465
+ setMessages([{ role: "assistant", content: "Failed to connect to MCP server." }]);
466
+ }
467
+ }
468
+ };
469
+ const handleSelectTool = (value) => {
470
+ setSelectedToolName(value);
471
+ setParamValues({});
472
+ };
473
+ const handleExecute = async () => {
474
+ var _a2;
475
+ if (!selectedTool) return;
476
+ setLoading(true);
477
+ const args = {};
478
+ const props = ((_a2 = selectedTool.inputSchema) == null ? void 0 : _a2.properties) ?? {};
479
+ for (const [key, schema] of Object.entries(props)) {
480
+ const raw = paramValues[key];
481
+ if (raw === void 0 || raw === "") continue;
482
+ if (schema.type === "number" || schema.type === "integer") {
483
+ args[key] = Number(raw);
484
+ } else if (schema.type === "array") {
485
+ try {
486
+ args[key] = JSON.parse(raw);
487
+ } catch {
488
+ args[key] = raw.split(",").map((s) => s.trim());
489
+ }
490
+ } else if (schema.type === "object") {
491
+ try {
492
+ args[key] = JSON.parse(raw);
493
+ } catch {
494
+ args[key] = raw;
495
+ }
496
+ } else {
497
+ args[key] = raw;
498
+ }
499
+ }
500
+ setMessages((prev) => [
501
+ ...prev,
502
+ { role: "user", content: `${selectedTool.name}(${JSON.stringify(args)})` }
503
+ ]);
504
+ try {
505
+ const result = await mcpCallTool(selectedTool.name, args);
506
+ setMessages((prev) => [...prev, { role: "assistant", content: result }]);
507
+ } catch (err) {
508
+ setMessages((prev) => [
509
+ ...prev,
510
+ { role: "assistant", content: `Error: ${err.message ?? "Unknown error"}` }
511
+ ]);
512
+ }
513
+ setLoading(false);
514
+ setSelectedToolName("");
515
+ setParamValues({});
516
+ };
517
+ const allParams = selectedTool ? [
518
+ ...Object.entries(((_a = selectedTool.inputSchema) == null ? void 0 : _a.properties) ?? {}).filter(
519
+ ([key]) => {
520
+ var _a2;
521
+ return (((_a2 = selectedTool.inputSchema) == null ? void 0 : _a2.required) ?? []).includes(key);
522
+ }
523
+ ),
524
+ ...Object.entries(((_b = selectedTool.inputSchema) == null ? void 0 : _b.properties) ?? {}).filter(
525
+ ([key]) => {
526
+ var _a2;
527
+ return !(((_a2 = selectedTool.inputSchema) == null ? void 0 : _a2.required) ?? []).includes(key);
528
+ }
529
+ )
530
+ ] : [];
531
+ return /* @__PURE__ */ jsxs(Drawer, { open, onOpenChange: handleOpen, children: [
532
+ /* @__PURE__ */ jsx(Drawer.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { size: "small", children: [
533
+ /* @__PURE__ */ jsx(ChatBubbleLeftRight, { className: "mr-1" }),
534
+ "Query Data"
535
+ ] }) }),
536
+ /* @__PURE__ */ jsxs(Drawer.Content, { children: [
537
+ /* @__PURE__ */ jsxs(Drawer.Header, { children: [
538
+ /* @__PURE__ */ jsx(Drawer.Title, { children: "Query Data" }),
539
+ /* @__PURE__ */ jsx(Drawer.Description, { children: "Run queries against your store data using MCP tools." })
540
+ ] }),
541
+ /* @__PURE__ */ jsxs(Drawer.Body, { className: "flex flex-col gap-4 overflow-hidden", children: [
542
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
543
+ /* @__PURE__ */ jsxs(Select, { value: selectedToolName, onValueChange: handleSelectTool, children: [
544
+ /* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "Select a tool..." }) }),
545
+ /* @__PURE__ */ jsx(Select.Content, { children: tools.map((tool) => /* @__PURE__ */ jsx(Select.Item, { value: tool.name, children: tool.name }, tool.name)) })
546
+ ] }),
547
+ selectedTool && /* @__PURE__ */ jsxs(Fragment, { children: [
548
+ /* @__PURE__ */ jsx(Text, { size: "xsmall", className: "text-ui-fg-subtle", children: selectedTool.description }),
549
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2 items-end", children: [
550
+ allParams.map(([key, schema]) => {
551
+ var _a2;
552
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
553
+ /* @__PURE__ */ jsxs(Text, { size: "xsmall", className: "text-ui-fg-subtle", children: [
554
+ key,
555
+ (((_a2 = selectedTool.inputSchema) == null ? void 0 : _a2.required) ?? []).includes(key) ? " *" : ""
556
+ ] }),
557
+ /* @__PURE__ */ jsx(
558
+ Input,
559
+ {
560
+ size: "small",
561
+ placeholder: schema.description ?? key,
562
+ value: paramValues[key] ?? "",
563
+ onChange: (e) => setParamValues((prev) => ({ ...prev, [key]: e.target.value }))
564
+ }
565
+ )
566
+ ] }, key);
567
+ }),
568
+ /* @__PURE__ */ jsx(Button, { size: "small", onClick: handleExecute, disabled: loading, children: "Run" })
569
+ ] })
570
+ ] })
571
+ ] }),
572
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto flex flex-col gap-2 min-h-0", children: [
573
+ messages.map(
574
+ (msg, i) => msg.role === "user" ? /* @__PURE__ */ jsx(
575
+ "div",
576
+ {
577
+ className: "max-w-[90%] self-end rounded-lg px-3 py-2 text-xs bg-ui-bg-subtle text-ui-fg-base",
578
+ children: /* @__PURE__ */ jsx("pre", { className: "whitespace-pre-wrap font-mono break-all", children: msg.content })
579
+ },
580
+ i
581
+ ) : /* @__PURE__ */ jsx("div", { className: "self-start w-full", children: /* @__PURE__ */ jsx(
582
+ CodeBlock,
583
+ {
584
+ snippets: [
585
+ {
586
+ label: "Result",
587
+ language: "json",
588
+ code: msg.content,
589
+ hideLineNumbers: true
590
+ }
591
+ ],
592
+ children: /* @__PURE__ */ jsx(CodeBlock.Body, { className: "text-xs [&_code]:text-xs" })
593
+ }
594
+ ) }, i)
595
+ ),
596
+ loading && /* @__PURE__ */ jsx(Text, { size: "xsmall", className: "text-ui-fg-muted", children: "Running..." }),
597
+ /* @__PURE__ */ jsx("div", { ref: messagesEndRef })
598
+ ] })
599
+ ] })
600
+ ] })
601
+ ] });
602
+ };
603
+ const config = defineRouteConfig({
604
+ label: "Statistics",
605
+ icon: ChartBar,
606
+ rank: 0
607
+ });
608
+ const handle = { breadcrumb: () => "Statistics" };
609
+ const PERIOD_LABELS = {
610
+ today: "Today",
611
+ week: "This Week",
612
+ month: "This Month"
613
+ };
614
+ function buildDefaultLayouts() {
615
+ return {
616
+ lg: WIDGETS.map((w) => ({ i: w.id, ...w.layouts.lg })),
617
+ md: WIDGETS.map((w) => ({ i: w.id, ...w.layouts.md })),
618
+ sm: WIDGETS.map((w) => ({ i: w.id, ...w.layouts.sm }))
619
+ };
620
+ }
621
+ function mergeLayouts(saved, widgets) {
622
+ const defaults = buildDefaultLayouts();
623
+ if (!saved || saved.length === 0) return defaults;
624
+ const savedMap = new Map(saved.map((s) => [s.widget_id, s]));
625
+ return {
626
+ ...defaults,
627
+ lg: widgets.map((w) => {
628
+ const s = savedMap.get(w.id);
629
+ if (s) return { i: w.id, ...w.layouts.lg, x: s.x, y: s.y, w: s.w, h: s.h };
630
+ return { i: w.id, ...w.layouts.lg };
631
+ })
632
+ };
633
+ }
634
+ const StatisticsPage = () => {
635
+ const [period, setPeriod] = useState("week");
636
+ const [editMode, setEditMode] = useState(false);
637
+ const [layouts, setLayouts] = useState(buildDefaultLayouts);
638
+ const { width: gridWidth, containerRef: gridRef, mounted: gridMounted } = useContainerWidth({
639
+ measureBeforeMount: true
640
+ });
641
+ const { data: statsData, isLoading } = useStatistics(period);
642
+ const { data: layoutData } = useStatisticsLayout();
643
+ const { mutate: saveLayout } = useSaveStatisticsLayout();
644
+ const { mutate: recalculate, isPending: recalculating } = useRecalculateStatistics();
645
+ useEffect(() => {
646
+ if (layoutData == null ? void 0 : layoutData.layout) {
647
+ setLayouts(mergeLayouts(layoutData.layout, WIDGETS));
648
+ }
649
+ }, [layoutData]);
650
+ const handleLayoutChange = useCallback(
651
+ (_layout, allLayouts) => {
652
+ if (!editMode) return;
653
+ setLayouts((prev) => ({
654
+ ...prev,
655
+ ...Object.fromEntries(
656
+ Object.entries(allLayouts).map(([bp, l]) => [bp, [...l]])
657
+ )
658
+ }));
659
+ },
660
+ [editMode]
661
+ );
662
+ const handleSaveLayout = () => {
663
+ const payload = layouts.lg.map((l) => ({
664
+ widget_id: l.i,
665
+ x: l.x,
666
+ y: l.y,
667
+ w: l.w,
668
+ h: l.h,
669
+ visible: true
670
+ }));
671
+ saveLayout(payload, {
672
+ onSuccess: () => {
673
+ toast.success("Layout saved");
674
+ setEditMode(false);
675
+ },
676
+ onError: () => toast.error("Failed to save layout")
677
+ });
678
+ };
679
+ const widgetMap = useMemo(
680
+ () => new Map(WIDGETS.map((w) => [w.id, w])),
681
+ []
682
+ );
683
+ const statistics = (statsData == null ? void 0 : statsData.statistics) ?? [];
684
+ const totals = (statsData == null ? void 0 : statsData.totals) ?? {
685
+ revenue_total: 0,
686
+ order_count: 0,
687
+ average_order_value: 0,
688
+ new_customer_count: 0,
689
+ returning_customer_count: 0,
690
+ pending_fulfillment_count: 0,
691
+ low_stock_count: 0
692
+ };
693
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 p-4", children: [
694
+ /* @__PURE__ */ jsx(Container, { className: "p-0", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 px-6 py-4 md:flex-row md:items-center md:justify-between", children: [
695
+ /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Statistics" }),
696
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
697
+ /* @__PURE__ */ jsx("div", { className: "flex rounded-lg border border-ui-border-base overflow-hidden", children: Object.keys(PERIOD_LABELS).map((p) => /* @__PURE__ */ jsx(
698
+ "button",
699
+ {
700
+ onClick: () => setPeriod(p),
701
+ className: `px-3 py-1.5 text-xs font-medium transition-colors ${period === p ? "bg-ui-bg-base-pressed text-ui-fg-base" : "text-ui-fg-subtle hover:bg-ui-bg-base-hover"}`,
702
+ children: PERIOD_LABELS[p]
703
+ },
704
+ p
705
+ )) }),
706
+ /* @__PURE__ */ jsx(
707
+ Button,
708
+ {
709
+ size: "small",
710
+ variant: "secondary",
711
+ onClick: () => recalculate(void 0, {
712
+ onSuccess: () => toast.success("Statistics recalculated"),
713
+ onError: () => toast.error("Recalculation failed")
714
+ }),
715
+ isLoading: recalculating,
716
+ children: "Recalculate"
717
+ }
718
+ ),
719
+ editMode ? /* @__PURE__ */ jsxs(Fragment, { children: [
720
+ /* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", onClick: () => setEditMode(false), children: "Cancel" }),
721
+ /* @__PURE__ */ jsx(Button, { size: "small", onClick: handleSaveLayout, children: "Save Layout" })
722
+ ] }) : /* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", onClick: () => setEditMode(true), children: "Customize" }),
723
+ /* @__PURE__ */ jsx(McpQuery, {})
724
+ ] })
725
+ ] }) }),
726
+ /* @__PURE__ */ jsx("div", { ref: gridRef, className: editMode ? "ring-2 ring-ui-border-interactive ring-offset-2 rounded-lg" : "", children: isLoading ? /* @__PURE__ */ jsx(Container, { className: "p-6", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted", children: "Loading statistics..." }) }) : gridMounted && /* @__PURE__ */ jsx(
727
+ ResponsiveGridLayout,
728
+ {
729
+ width: gridWidth,
730
+ layouts,
731
+ breakpoints: { lg: 1200, md: 768, sm: 480 },
732
+ cols: { lg: 12, md: 8, sm: 4 },
733
+ rowHeight: 80,
734
+ dragConfig: { enabled: editMode, handle: ".drag-handle" },
735
+ resizeConfig: { enabled: editMode },
736
+ onLayoutChange: handleLayoutChange,
737
+ containerPadding: [0, 0],
738
+ compactor: verticalCompactor,
739
+ children: layouts.lg.map((l) => {
740
+ const widget = widgetMap.get(l.i);
741
+ if (!widget) return null;
742
+ const Component = widget.component;
743
+ return /* @__PURE__ */ jsxs("div", { className: "relative h-full", children: [
744
+ editMode && /* @__PURE__ */ jsx("div", { className: "drag-handle absolute top-0 left-0 right-0 h-6 bg-ui-bg-subtle-hover cursor-move rounded-t-lg flex items-center justify-center", children: /* @__PURE__ */ jsx(Text, { size: "xsmall", className: "text-ui-fg-muted", children: "Drag to move" }) }),
745
+ /* @__PURE__ */ jsx(Component, { statistics, totals, period })
746
+ ] }, l.i);
747
+ })
748
+ }
749
+ ) })
750
+ ] });
751
+ };
752
+ const widgetModule = { widgets: [] };
753
+ const routeModule = {
754
+ routes: [
755
+ {
756
+ Component: StatisticsPage,
757
+ path: "/statistics",
758
+ handle
759
+ }
760
+ ]
761
+ };
762
+ const menuItemModule = {
763
+ menuItems: [
764
+ {
765
+ label: config.label,
766
+ icon: config.icon,
767
+ path: "/statistics",
768
+ nested: void 0,
769
+ rank: 0,
770
+ translationNs: void 0
771
+ }
772
+ ]
773
+ };
774
+ const formModule = { customFields: {} };
775
+ const displayModule = {
776
+ displays: {}
777
+ };
778
+ const i18nModule = { resources: {} };
779
+ const plugin = {
780
+ widgetModule,
781
+ routeModule,
782
+ menuItemModule,
783
+ formModule,
784
+ displayModule,
785
+ i18nModule
786
+ };
787
+ export {
788
+ plugin as default
789
+ };