medusa-analytics 0.0.1

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.
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+ const jsxRuntime = require("react/jsx-runtime");
3
+ const react = require("react");
4
+ const adminSdk = require("@medusajs/admin-sdk");
5
+ const icons = require("@medusajs/icons");
6
+ const ui = require("@medusajs/ui");
7
+ const recharts = require("recharts");
8
+ const STATUS_COLORS = {
9
+ pending: "#F59E0B",
10
+ confirmed: "#3B82F6",
11
+ processing: "#8B5CF6",
12
+ shipped: "#06B6D4",
13
+ delivered: "#10B981",
14
+ cancelled: "#EF4444"
15
+ };
16
+ const STATUS_ORDER = [
17
+ "pending",
18
+ "confirmed",
19
+ "processing",
20
+ "shipped",
21
+ "delivered",
22
+ "cancelled"
23
+ ];
24
+ function formatCurrency(value) {
25
+ return new Intl.NumberFormat("en-IN", {
26
+ style: "currency",
27
+ currency: "INR",
28
+ minimumFractionDigits: 0,
29
+ maximumFractionDigits: 0
30
+ }).format(value);
31
+ }
32
+ function OrdersDashboard() {
33
+ const [data, setData] = react.useState(null);
34
+ const [loading, setLoading] = react.useState(true);
35
+ const [error, setError] = react.useState(null);
36
+ const [filter, setFilter] = react.useState("7");
37
+ react.useEffect(() => {
38
+ let cancelled = false;
39
+ setLoading(true);
40
+ setError(null);
41
+ const url = `/admin/analytics/orders-summary?days=${filter}`;
42
+ if (typeof fetch !== "undefined") {
43
+ fetch("http://127.0.0.1:7242/ingest/47f434f8-0643-48e5-bd1a-edfe05973288", { method: "POST", headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "21bff8" }, body: JSON.stringify({ sessionId: "21bff8", location: "OrdersDashboard.tsx:useEffect", message: "OrdersDashboard fetch", data: { filter, url }, timestamp: Date.now(), hypothesisId: "C" }) }).catch(() => {
44
+ });
45
+ }
46
+ fetch(url).then((res) => {
47
+ if (typeof fetch !== "undefined") {
48
+ fetch("http://127.0.0.1:7242/ingest/47f434f8-0643-48e5-bd1a-edfe05973288", { method: "POST", headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "21bff8" }, body: JSON.stringify({ sessionId: "21bff8", location: "OrdersDashboard.tsx:fetch.then", message: "API response", data: { status: res.status, statusText: res.statusText, url: res.url }, timestamp: Date.now(), hypothesisId: "C" }) }).catch(() => {
49
+ });
50
+ }
51
+ if (!res.ok) throw new Error(res.statusText);
52
+ return res.json();
53
+ }).then((body) => {
54
+ if (!cancelled) setData(body);
55
+ }).catch((e) => {
56
+ if (!cancelled) setError(e instanceof Error ? e.message : String(e));
57
+ }).finally(() => {
58
+ if (!cancelled) setLoading(false);
59
+ });
60
+ return () => {
61
+ cancelled = true;
62
+ };
63
+ }, [filter]);
64
+ if (loading) {
65
+ return /* @__PURE__ */ jsxRuntime.jsx(
66
+ "div",
67
+ {
68
+ className: "flex items-center justify-center min-h-[320px]",
69
+ role: "status",
70
+ "aria-label": "Loading analytics",
71
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted", children: "Loading…" })
72
+ }
73
+ );
74
+ }
75
+ if (error || !data) {
76
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "p-6", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-danger", children: error ?? "Failed to load analytics" }) });
77
+ }
78
+ const pieData = STATUS_ORDER.filter((k) => (data.statusCounts[k] ?? 0) > 0).map(
79
+ (key) => ({
80
+ name: key.charAt(0).toUpperCase() + key.slice(1),
81
+ value: data.statusCounts[key] ?? 0,
82
+ color: STATUS_COLORS[key]
83
+ })
84
+ );
85
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-8", children: [
86
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-4", children: [
87
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Orders Analytics" }),
88
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
89
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "analytics-period", className: "text-ui-fg-muted text-sm", children: "Period" }),
90
+ /* @__PURE__ */ jsxRuntime.jsxs(
91
+ "select",
92
+ {
93
+ id: "analytics-period",
94
+ value: filter,
95
+ onChange: (e) => setFilter(e.target.value),
96
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive",
97
+ children: [
98
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "0", children: "Today's orders" }),
99
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "7", children: "Last 7 days" }),
100
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "30", children: "Last 30 days" }),
101
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "90", children: "Last 90 days" })
102
+ ]
103
+ }
104
+ )
105
+ ] })
106
+ ] }),
107
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4", children: [
108
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-4", children: [
109
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Total orders" }),
110
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: data.totalOrders.toLocaleString() })
111
+ ] }),
112
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-4", children: [
113
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Total revenue" }),
114
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: formatCurrency(data.totalRevenue) })
115
+ ] }),
116
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-4", children: [
117
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Average order value" }),
118
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: formatCurrency(data.aov) })
119
+ ] }),
120
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-4", children: [
121
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Orders today" }),
122
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: data.ordersToday.toLocaleString() })
123
+ ] })
124
+ ] }),
125
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-2 gap-6", children: [
126
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-4", children: [
127
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm mb-2", children: "Pending orders" }),
128
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: data.pendingOrders.toLocaleString() })
129
+ ] }),
130
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-4", children: [
131
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm mb-2", children: "Avg. fulfillment time (hours)" }),
132
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: data.avgFulfillmentTimeHours != null ? `${data.avgFulfillmentTimeHours}h` : "—" })
133
+ ] })
134
+ ] }),
135
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-6", children: [
136
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", className: "mb-4", children: "Orders over time" }),
137
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-72", children: /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsxs(
138
+ recharts.LineChart,
139
+ {
140
+ data: data.dailyOrders,
141
+ margin: { top: 8, right: 8, left: 8, bottom: 8 },
142
+ children: [
143
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.CartesianGrid, { strokeDasharray: "3 3", className: "stroke-ui-border-base" }),
144
+ /* @__PURE__ */ jsxRuntime.jsx(
145
+ recharts.XAxis,
146
+ {
147
+ dataKey: "date",
148
+ tick: { fontSize: 12 },
149
+ tickFormatter: (v) => {
150
+ const d = new Date(v);
151
+ return `${d.getUTCMonth() + 1}/${d.getUTCDate()}`;
152
+ }
153
+ }
154
+ ),
155
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.YAxis, { tick: { fontSize: 12 } }),
156
+ /* @__PURE__ */ jsxRuntime.jsx(
157
+ recharts.Tooltip,
158
+ {
159
+ labelFormatter: (v) => new Date(v).toLocaleDateString(),
160
+ formatter: (value) => [value ?? 0, "Orders"]
161
+ }
162
+ ),
163
+ /* @__PURE__ */ jsxRuntime.jsx(
164
+ recharts.Line,
165
+ {
166
+ type: "monotone",
167
+ dataKey: "orders_count",
168
+ name: "Orders",
169
+ stroke: "var(--medusa-color-ui-fg-interactive)",
170
+ strokeWidth: 2,
171
+ dot: { r: 3 }
172
+ }
173
+ )
174
+ ]
175
+ }
176
+ ) }) })
177
+ ] }),
178
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-6", children: [
179
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", className: "mb-4", children: "Orders by status" }),
180
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-72", children: /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsxs(recharts.PieChart, { children: [
181
+ /* @__PURE__ */ jsxRuntime.jsx(
182
+ recharts.Pie,
183
+ {
184
+ data: pieData,
185
+ cx: "50%",
186
+ cy: "50%",
187
+ innerRadius: 60,
188
+ outerRadius: 100,
189
+ paddingAngle: 2,
190
+ dataKey: "value",
191
+ nameKey: "name",
192
+ label: ({ name, value }) => `${name}: ${value}`,
193
+ children: pieData.map((entry, index) => /* @__PURE__ */ jsxRuntime.jsx(recharts.Cell, { fill: entry.color }, `cell-${index}`))
194
+ }
195
+ ),
196
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Tooltip, { formatter: (value) => [value ?? 0, "Orders"] }),
197
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Legend, {})
198
+ ] }) }) })
199
+ ] })
200
+ ] });
201
+ }
202
+ const ANALYTICS_MODULES = [{ id: "orders", label: "Orders" }];
203
+ const PLUGIN_VERSION = "0.2";
204
+ const AnalyticsPage = () => {
205
+ const [activeModule, setActiveModule] = react.useState("orders");
206
+ if (typeof fetch !== "undefined") {
207
+ fetch("http://127.0.0.1:7242/ingest/47f434f8-0643-48e5-bd1a-edfe05973288", { method: "POST", headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "21bff8" }, body: JSON.stringify({ sessionId: "21bff8", location: "page.tsx:AnalyticsPage", message: "Analytics page mounted", data: { pluginVersion: PLUGIN_VERSION, activeModule }, timestamp: Date.now(), hypothesisId: "A" }) }).catch(() => {
208
+ });
209
+ }
210
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full bg-ui-bg-base", children: [
211
+ /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: "w-56 shrink-0 border-r border-ui-border-base bg-ui-bg-subtle p-4 flex flex-col", children: [
212
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", className: "mb-3 text-ui-fg-subtle", children: "Analysis" }),
213
+ /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "flex flex-col gap-1 flex-1", children: ANALYTICS_MODULES.map((m) => /* @__PURE__ */ jsxRuntime.jsx(
214
+ ui.Button,
215
+ {
216
+ variant: activeModule === m.id ? "secondary" : "transparent",
217
+ className: "w-full justify-start",
218
+ onClick: () => setActiveModule(m.id),
219
+ children: m.label
220
+ },
221
+ m.id
222
+ )) }),
223
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { className: "text-ui-fg-muted text-xs mt-4", "data-analytics-build": PLUGIN_VERSION, children: [
224
+ "Analytics v",
225
+ PLUGIN_VERSION
226
+ ] })
227
+ ] }),
228
+ /* @__PURE__ */ jsxRuntime.jsx("main", { className: "flex-1 overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "py-6 px-6", children: activeModule === "orders" && /* @__PURE__ */ jsxRuntime.jsx(OrdersDashboard, {}) }) }) })
229
+ ] });
230
+ };
231
+ const config = adminSdk.defineRouteConfig({
232
+ label: "Analytics",
233
+ icon: icons.ChartBar,
234
+ rank: 0
235
+ });
236
+ const i18nTranslations0 = {};
237
+ const widgetModule = { widgets: [] };
238
+ const routeModule = {
239
+ routes: [
240
+ {
241
+ Component: AnalyticsPage,
242
+ path: "/analytics"
243
+ }
244
+ ]
245
+ };
246
+ const menuItemModule = {
247
+ menuItems: [
248
+ {
249
+ label: config.label,
250
+ icon: config.icon,
251
+ path: "/analytics",
252
+ nested: void 0,
253
+ rank: 0,
254
+ translationNs: void 0
255
+ }
256
+ ]
257
+ };
258
+ const formModule = { customFields: {} };
259
+ const displayModule = {
260
+ displays: {}
261
+ };
262
+ const i18nModule = { resources: i18nTranslations0 };
263
+ const plugin = {
264
+ widgetModule,
265
+ routeModule,
266
+ menuItemModule,
267
+ formModule,
268
+ displayModule,
269
+ i18nModule
270
+ };
271
+ module.exports = plugin;
@@ -0,0 +1,272 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { defineRouteConfig } from "@medusajs/admin-sdk";
4
+ import { ChartBar } from "@medusajs/icons";
5
+ import { Text, Container, Heading, Label, Button } from "@medusajs/ui";
6
+ import { ResponsiveContainer, LineChart, CartesianGrid, XAxis, YAxis, Tooltip, Line, PieChart, Pie, Cell, Legend } from "recharts";
7
+ const STATUS_COLORS = {
8
+ pending: "#F59E0B",
9
+ confirmed: "#3B82F6",
10
+ processing: "#8B5CF6",
11
+ shipped: "#06B6D4",
12
+ delivered: "#10B981",
13
+ cancelled: "#EF4444"
14
+ };
15
+ const STATUS_ORDER = [
16
+ "pending",
17
+ "confirmed",
18
+ "processing",
19
+ "shipped",
20
+ "delivered",
21
+ "cancelled"
22
+ ];
23
+ function formatCurrency(value) {
24
+ return new Intl.NumberFormat("en-IN", {
25
+ style: "currency",
26
+ currency: "INR",
27
+ minimumFractionDigits: 0,
28
+ maximumFractionDigits: 0
29
+ }).format(value);
30
+ }
31
+ function OrdersDashboard() {
32
+ const [data, setData] = useState(null);
33
+ const [loading, setLoading] = useState(true);
34
+ const [error, setError] = useState(null);
35
+ const [filter, setFilter] = useState("7");
36
+ useEffect(() => {
37
+ let cancelled = false;
38
+ setLoading(true);
39
+ setError(null);
40
+ const url = `/admin/analytics/orders-summary?days=${filter}`;
41
+ if (typeof fetch !== "undefined") {
42
+ fetch("http://127.0.0.1:7242/ingest/47f434f8-0643-48e5-bd1a-edfe05973288", { method: "POST", headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "21bff8" }, body: JSON.stringify({ sessionId: "21bff8", location: "OrdersDashboard.tsx:useEffect", message: "OrdersDashboard fetch", data: { filter, url }, timestamp: Date.now(), hypothesisId: "C" }) }).catch(() => {
43
+ });
44
+ }
45
+ fetch(url).then((res) => {
46
+ if (typeof fetch !== "undefined") {
47
+ fetch("http://127.0.0.1:7242/ingest/47f434f8-0643-48e5-bd1a-edfe05973288", { method: "POST", headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "21bff8" }, body: JSON.stringify({ sessionId: "21bff8", location: "OrdersDashboard.tsx:fetch.then", message: "API response", data: { status: res.status, statusText: res.statusText, url: res.url }, timestamp: Date.now(), hypothesisId: "C" }) }).catch(() => {
48
+ });
49
+ }
50
+ if (!res.ok) throw new Error(res.statusText);
51
+ return res.json();
52
+ }).then((body) => {
53
+ if (!cancelled) setData(body);
54
+ }).catch((e) => {
55
+ if (!cancelled) setError(e instanceof Error ? e.message : String(e));
56
+ }).finally(() => {
57
+ if (!cancelled) setLoading(false);
58
+ });
59
+ return () => {
60
+ cancelled = true;
61
+ };
62
+ }, [filter]);
63
+ if (loading) {
64
+ return /* @__PURE__ */ jsx(
65
+ "div",
66
+ {
67
+ className: "flex items-center justify-center min-h-[320px]",
68
+ role: "status",
69
+ "aria-label": "Loading analytics",
70
+ children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted", children: "Loading…" })
71
+ }
72
+ );
73
+ }
74
+ if (error || !data) {
75
+ return /* @__PURE__ */ jsx(Container, { className: "p-6", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-danger", children: error ?? "Failed to load analytics" }) });
76
+ }
77
+ const pieData = STATUS_ORDER.filter((k) => (data.statusCounts[k] ?? 0) > 0).map(
78
+ (key) => ({
79
+ name: key.charAt(0).toUpperCase() + key.slice(1),
80
+ value: data.statusCounts[key] ?? 0,
81
+ color: STATUS_COLORS[key]
82
+ })
83
+ );
84
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-8", children: [
85
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-4", children: [
86
+ /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Orders Analytics" }),
87
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
88
+ /* @__PURE__ */ jsx(Label, { htmlFor: "analytics-period", className: "text-ui-fg-muted text-sm", children: "Period" }),
89
+ /* @__PURE__ */ jsxs(
90
+ "select",
91
+ {
92
+ id: "analytics-period",
93
+ value: filter,
94
+ onChange: (e) => setFilter(e.target.value),
95
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive",
96
+ children: [
97
+ /* @__PURE__ */ jsx("option", { value: "0", children: "Today's orders" }),
98
+ /* @__PURE__ */ jsx("option", { value: "7", children: "Last 7 days" }),
99
+ /* @__PURE__ */ jsx("option", { value: "30", children: "Last 30 days" }),
100
+ /* @__PURE__ */ jsx("option", { value: "90", children: "Last 90 days" })
101
+ ]
102
+ }
103
+ )
104
+ ] })
105
+ ] }),
106
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4", children: [
107
+ /* @__PURE__ */ jsxs(Container, { className: "p-4", children: [
108
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Total orders" }),
109
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: data.totalOrders.toLocaleString() })
110
+ ] }),
111
+ /* @__PURE__ */ jsxs(Container, { className: "p-4", children: [
112
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Total revenue" }),
113
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: formatCurrency(data.totalRevenue) })
114
+ ] }),
115
+ /* @__PURE__ */ jsxs(Container, { className: "p-4", children: [
116
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Average order value" }),
117
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: formatCurrency(data.aov) })
118
+ ] }),
119
+ /* @__PURE__ */ jsxs(Container, { className: "p-4", children: [
120
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Orders today" }),
121
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: data.ordersToday.toLocaleString() })
122
+ ] })
123
+ ] }),
124
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-2 gap-6", children: [
125
+ /* @__PURE__ */ jsxs(Container, { className: "p-4", children: [
126
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm mb-2", children: "Pending orders" }),
127
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: data.pendingOrders.toLocaleString() })
128
+ ] }),
129
+ /* @__PURE__ */ jsxs(Container, { className: "p-4", children: [
130
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm mb-2", children: "Avg. fulfillment time (hours)" }),
131
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: data.avgFulfillmentTimeHours != null ? `${data.avgFulfillmentTimeHours}h` : "—" })
132
+ ] })
133
+ ] }),
134
+ /* @__PURE__ */ jsxs(Container, { className: "p-6", children: [
135
+ /* @__PURE__ */ jsx(Heading, { level: "h3", className: "mb-4", children: "Orders over time" }),
136
+ /* @__PURE__ */ jsx("div", { className: "h-72", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(
137
+ LineChart,
138
+ {
139
+ data: data.dailyOrders,
140
+ margin: { top: 8, right: 8, left: 8, bottom: 8 },
141
+ children: [
142
+ /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", className: "stroke-ui-border-base" }),
143
+ /* @__PURE__ */ jsx(
144
+ XAxis,
145
+ {
146
+ dataKey: "date",
147
+ tick: { fontSize: 12 },
148
+ tickFormatter: (v) => {
149
+ const d = new Date(v);
150
+ return `${d.getUTCMonth() + 1}/${d.getUTCDate()}`;
151
+ }
152
+ }
153
+ ),
154
+ /* @__PURE__ */ jsx(YAxis, { tick: { fontSize: 12 } }),
155
+ /* @__PURE__ */ jsx(
156
+ Tooltip,
157
+ {
158
+ labelFormatter: (v) => new Date(v).toLocaleDateString(),
159
+ formatter: (value) => [value ?? 0, "Orders"]
160
+ }
161
+ ),
162
+ /* @__PURE__ */ jsx(
163
+ Line,
164
+ {
165
+ type: "monotone",
166
+ dataKey: "orders_count",
167
+ name: "Orders",
168
+ stroke: "var(--medusa-color-ui-fg-interactive)",
169
+ strokeWidth: 2,
170
+ dot: { r: 3 }
171
+ }
172
+ )
173
+ ]
174
+ }
175
+ ) }) })
176
+ ] }),
177
+ /* @__PURE__ */ jsxs(Container, { className: "p-6", children: [
178
+ /* @__PURE__ */ jsx(Heading, { level: "h3", className: "mb-4", children: "Orders by status" }),
179
+ /* @__PURE__ */ jsx("div", { className: "h-72", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(PieChart, { children: [
180
+ /* @__PURE__ */ jsx(
181
+ Pie,
182
+ {
183
+ data: pieData,
184
+ cx: "50%",
185
+ cy: "50%",
186
+ innerRadius: 60,
187
+ outerRadius: 100,
188
+ paddingAngle: 2,
189
+ dataKey: "value",
190
+ nameKey: "name",
191
+ label: ({ name, value }) => `${name}: ${value}`,
192
+ children: pieData.map((entry, index) => /* @__PURE__ */ jsx(Cell, { fill: entry.color }, `cell-${index}`))
193
+ }
194
+ ),
195
+ /* @__PURE__ */ jsx(Tooltip, { formatter: (value) => [value ?? 0, "Orders"] }),
196
+ /* @__PURE__ */ jsx(Legend, {})
197
+ ] }) }) })
198
+ ] })
199
+ ] });
200
+ }
201
+ const ANALYTICS_MODULES = [{ id: "orders", label: "Orders" }];
202
+ const PLUGIN_VERSION = "0.2";
203
+ const AnalyticsPage = () => {
204
+ const [activeModule, setActiveModule] = useState("orders");
205
+ if (typeof fetch !== "undefined") {
206
+ fetch("http://127.0.0.1:7242/ingest/47f434f8-0643-48e5-bd1a-edfe05973288", { method: "POST", headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "21bff8" }, body: JSON.stringify({ sessionId: "21bff8", location: "page.tsx:AnalyticsPage", message: "Analytics page mounted", data: { pluginVersion: PLUGIN_VERSION, activeModule }, timestamp: Date.now(), hypothesisId: "A" }) }).catch(() => {
207
+ });
208
+ }
209
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full bg-ui-bg-base", children: [
210
+ /* @__PURE__ */ jsxs("aside", { className: "w-56 shrink-0 border-r border-ui-border-base bg-ui-bg-subtle p-4 flex flex-col", children: [
211
+ /* @__PURE__ */ jsx(Heading, { level: "h3", className: "mb-3 text-ui-fg-subtle", children: "Analysis" }),
212
+ /* @__PURE__ */ jsx("nav", { className: "flex flex-col gap-1 flex-1", children: ANALYTICS_MODULES.map((m) => /* @__PURE__ */ jsx(
213
+ Button,
214
+ {
215
+ variant: activeModule === m.id ? "secondary" : "transparent",
216
+ className: "w-full justify-start",
217
+ onClick: () => setActiveModule(m.id),
218
+ children: m.label
219
+ },
220
+ m.id
221
+ )) }),
222
+ /* @__PURE__ */ jsxs(Text, { className: "text-ui-fg-muted text-xs mt-4", "data-analytics-build": PLUGIN_VERSION, children: [
223
+ "Analytics v",
224
+ PLUGIN_VERSION
225
+ ] })
226
+ ] }),
227
+ /* @__PURE__ */ jsx("main", { className: "flex-1 overflow-auto", children: /* @__PURE__ */ jsx(Container, { className: "divide-y p-0", children: /* @__PURE__ */ jsx("div", { className: "py-6 px-6", children: activeModule === "orders" && /* @__PURE__ */ jsx(OrdersDashboard, {}) }) }) })
228
+ ] });
229
+ };
230
+ const config = defineRouteConfig({
231
+ label: "Analytics",
232
+ icon: ChartBar,
233
+ rank: 0
234
+ });
235
+ const i18nTranslations0 = {};
236
+ const widgetModule = { widgets: [] };
237
+ const routeModule = {
238
+ routes: [
239
+ {
240
+ Component: AnalyticsPage,
241
+ path: "/analytics"
242
+ }
243
+ ]
244
+ };
245
+ const menuItemModule = {
246
+ menuItems: [
247
+ {
248
+ label: config.label,
249
+ icon: config.icon,
250
+ path: "/analytics",
251
+ nested: void 0,
252
+ rank: 0,
253
+ translationNs: void 0
254
+ }
255
+ ]
256
+ };
257
+ const formModule = { customFields: {} };
258
+ const displayModule = {
259
+ displays: {}
260
+ };
261
+ const i18nModule = { resources: i18nTranslations0 };
262
+ const plugin = {
263
+ widgetModule,
264
+ routeModule,
265
+ menuItemModule,
266
+ formModule,
267
+ displayModule,
268
+ i18nModule
269
+ };
270
+ export {
271
+ plugin as default
272
+ };
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GET = GET;
4
+ const utils_1 = require("@medusajs/framework/utils");
5
+ const STATUS_BUCKETS = [
6
+ "pending",
7
+ "confirmed",
8
+ "processing",
9
+ "shipped",
10
+ "delivered",
11
+ "cancelled",
12
+ ];
13
+ function normalizeStatus(status) {
14
+ if (!status || typeof status !== "string")
15
+ return "pending";
16
+ const s = status.toLowerCase();
17
+ if (s === "canceled" || s === "cancelled")
18
+ return "cancelled";
19
+ if (STATUS_BUCKETS.includes(s))
20
+ return s;
21
+ return "pending";
22
+ }
23
+ function isPendingStatus(status) {
24
+ const normalized = normalizeStatus(status);
25
+ return normalized !== "delivered" && normalized !== "cancelled";
26
+ }
27
+ function toDateKey(d) {
28
+ return d.toISOString().slice(0, 10);
29
+ }
30
+ async function GET(req, res) {
31
+ const remoteQuery = req.scope.resolve(utils_1.ContainerRegistrationKeys.REMOTE_QUERY);
32
+ const daysParam = req.query?.days;
33
+ const allowedDays = [0, 7, 30, 90];
34
+ const daysInput = typeof daysParam === "string" && allowedDays.includes(Number(daysParam))
35
+ ? Number(daysParam)
36
+ : 7;
37
+ const days = daysInput === 0 ? 1 : daysInput;
38
+ // #region agent log
39
+ fetch('http://127.0.0.1:7242/ingest/47f434f8-0643-48e5-bd1a-edfe05973288', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '21bff8' }, body: JSON.stringify({ sessionId: '21bff8', location: 'orders-summary/route.ts:GET', message: 'orders-summary GET hit', data: { daysParam, daysInput, days }, timestamp: Date.now(), hypothesisId: 'C' }) }).catch(() => { });
40
+ // #endregion
41
+ const today = new Date();
42
+ today.setUTCHours(0, 0, 0, 0);
43
+ const todayKey = toDateKey(today);
44
+ const rangeStart = new Date(today);
45
+ rangeStart.setUTCDate(rangeStart.getUTCDate() - (days - 1));
46
+ rangeStart.setUTCHours(0, 0, 0, 0);
47
+ const rangeEnd = new Date(today);
48
+ rangeEnd.setUTCHours(23, 59, 59, 999);
49
+ try {
50
+ const queryObject = (0, utils_1.remoteQueryObjectFromString)({
51
+ entryPoint: "order",
52
+ fields: [
53
+ "id",
54
+ "status",
55
+ "total",
56
+ "created_at",
57
+ "fulfillments.id",
58
+ "fulfillments.shipped_at",
59
+ ],
60
+ take: 50000,
61
+ });
62
+ const ordersRaw = await remoteQuery(queryObject);
63
+ const allOrders = Array.isArray(ordersRaw) ? ordersRaw : ordersRaw ? [ordersRaw] : [];
64
+ const statusCounts = {};
65
+ for (const key of STATUS_BUCKETS) {
66
+ statusCounts[key] = 0;
67
+ }
68
+ let totalRevenue = 0;
69
+ let ordersTodayCount = 0;
70
+ let pendingOrders = 0;
71
+ const dailyMap = {};
72
+ const fulfillmentTimesMs = [];
73
+ for (const order of allOrders) {
74
+ const o = order;
75
+ const createdAt = o.created_at ? new Date(o.created_at) : null;
76
+ const dateKey = createdAt ? toDateKey(createdAt) : null;
77
+ const inRange = createdAt &&
78
+ dateKey &&
79
+ createdAt >= rangeStart &&
80
+ (daysInput === 0 ? dateKey === todayKey : createdAt <= rangeEnd);
81
+ if (!inRange)
82
+ continue;
83
+ const status = o.status;
84
+ const normalized = normalizeStatus(status);
85
+ statusCounts[normalized] = (statusCounts[normalized] ?? 0) + 1;
86
+ const total = typeof o.total === "number" ? o.total : 0;
87
+ totalRevenue += total;
88
+ if (dateKey === todayKey)
89
+ ordersTodayCount += 1;
90
+ if (isPendingStatus(status))
91
+ pendingOrders += 1;
92
+ if (dateKey) {
93
+ dailyMap[dateKey] = (dailyMap[dateKey] ?? 0) + 1;
94
+ }
95
+ const fulfillments = o.fulfillments ?? [];
96
+ for (const f of fulfillments) {
97
+ const shippedAt = f.shipped_at ? new Date(f.shipped_at) : null;
98
+ if (shippedAt && createdAt) {
99
+ fulfillmentTimesMs.push(shippedAt.getTime() - createdAt.getTime());
100
+ break;
101
+ }
102
+ }
103
+ }
104
+ const totalOrders = Object.values(dailyMap).reduce((s, n) => s + n, 0);
105
+ const aov = totalOrders > 0 ? totalRevenue / totalOrders : 0;
106
+ const dayCount = daysInput === 0 ? 1 : days;
107
+ const dailyOrders = [];
108
+ for (let i = 0; i < dayCount; i++) {
109
+ const d = new Date(rangeStart);
110
+ d.setUTCDate(d.getUTCDate() + i);
111
+ const key = toDateKey(d);
112
+ dailyOrders.push({
113
+ date: key,
114
+ orders_count: dailyMap[key] ?? 0,
115
+ });
116
+ }
117
+ dailyOrders.sort((a, b) => a.date.localeCompare(b.date));
118
+ let avgFulfillmentTimeHours = null;
119
+ if (fulfillmentTimesMs.length > 0) {
120
+ const avgMs = fulfillmentTimesMs.reduce((s, t) => s + t, 0) / fulfillmentTimesMs.length;
121
+ avgFulfillmentTimeHours = Math.round((avgMs / (1000 * 60 * 60)) * 10) / 10;
122
+ }
123
+ const body = {
124
+ totalOrders,
125
+ totalRevenue,
126
+ aov,
127
+ ordersToday: ordersTodayCount,
128
+ pendingOrders,
129
+ avgFulfillmentTimeHours,
130
+ statusCounts,
131
+ dailyOrders,
132
+ };
133
+ res.json(body);
134
+ }
135
+ catch (err) {
136
+ console.error("Orders summary analytics error:", err);
137
+ res.status(500).json({
138
+ error: "Failed to fetch orders summary",
139
+ message: err instanceof Error ? err.message : String(err),
140
+ });
141
+ }
142
+ }
143
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL2FuYWx5dGljcy9vcmRlcnMtc3VtbWFyeS9yb3V0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQW1DQSxrQkErSUM7QUFqTEQscURBQWtHO0FBUWxHLE1BQU0sY0FBYyxHQUFHO0lBQ3JCLFNBQVM7SUFDVCxXQUFXO0lBQ1gsWUFBWTtJQUNaLFNBQVM7SUFDVCxXQUFXO0lBQ1gsV0FBVztDQUNILENBQUE7QUFFVixTQUFTLGVBQWUsQ0FBQyxNQUEwQjtJQUNqRCxJQUFJLENBQUMsTUFBTSxJQUFJLE9BQU8sTUFBTSxLQUFLLFFBQVE7UUFBRSxPQUFPLFNBQVMsQ0FBQTtJQUMzRCxNQUFNLENBQUMsR0FBRyxNQUFNLENBQUMsV0FBVyxFQUFFLENBQUE7SUFDOUIsSUFBSSxDQUFDLEtBQUssVUFBVSxJQUFJLENBQUMsS0FBSyxXQUFXO1FBQUUsT0FBTyxXQUFXLENBQUE7SUFDN0QsSUFBSSxjQUFjLENBQUMsUUFBUSxDQUFDLENBQW9DLENBQUM7UUFBRSxPQUFPLENBQW9DLENBQUE7SUFDOUcsT0FBTyxTQUFTLENBQUE7QUFDbEIsQ0FBQztBQUVELFNBQVMsZUFBZSxDQUFDLE1BQTBCO0lBQ2pELE1BQU0sVUFBVSxHQUFHLGVBQWUsQ0FBQyxNQUFNLENBQUMsQ0FBQTtJQUMxQyxPQUFPLFVBQVUsS0FBSyxXQUFXLElBQUksVUFBVSxLQUFLLFdBQVcsQ0FBQTtBQUNqRSxDQUFDO0FBRUQsU0FBUyxTQUFTLENBQUMsQ0FBTztJQUN4QixPQUFPLENBQUMsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFBO0FBQ3JDLENBQUM7QUFFTSxLQUFLLFVBQVUsR0FBRyxDQUN2QixHQUFrQixFQUNsQixHQUFtQjtJQUVuQixNQUFNLFdBQVcsR0FBRyxHQUFHLENBQUMsS0FBSyxDQUFDLE9B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
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /**
3
+ * DTO for GET /admin/analytics/orders-summary response.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL2FuYWx5dGljcy9vcmRlcnMtc3VtbWFyeS90eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7O0dBRUcifQ==
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Plugin entry point. API routes and admin UI are discovered from the file structure.
5
+ * Export a minimal default so the package can be required when registered in medusa-config.
6
+ */
7
+ const plugin = {};
8
+ exports.default = plugin;
9
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQTs7O0dBR0c7QUFDSCxNQUFNLE1BQU0sR0FBRyxFQUFFLENBQUE7QUFDakIsa0JBQWUsTUFBTSxDQUFBIn0=
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ <p align="center">
2
+ <a href="https://www.medusajs.com">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/59018053/229103275-b5e482bb-4601-46e6-8142-244f531cebdb.svg">
5
+ <source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/59018053/229103726-e5b529a3-9b3f-4970-8a1f-c6af37f087bf.svg">
6
+ <img alt="Medusa logo" src="https://user-images.githubusercontent.com/59018053/229103726-e5b529a3-9b3f-4970-8a1f-c6af37f087bf.svg">
7
+ </picture>
8
+ </a>
9
+ </p>
10
+ <h1 align="center">
11
+ Medusa Plugin Starter
12
+ </h1>
13
+
14
+ <h4 align="center">
15
+ <a href="https://docs.medusajs.com">Documentation</a> |
16
+ <a href="https://www.medusajs.com">Website</a>
17
+ </h4>
18
+
19
+ <p align="center">
20
+ Building blocks for digital commerce
21
+ </p>
22
+ <p align="center">
23
+ <a href="https://github.com/medusajs/medusa/blob/master/CONTRIBUTING.md">
24
+ <img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="PRs welcome!" />
25
+ </a>
26
+ <a href="https://www.producthunt.com/posts/medusa"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Day-%23DA552E" alt="Product Hunt"></a>
27
+ <a href="https://discord.gg/xpCwq3Kfn8">
28
+ <img src="https://img.shields.io/badge/chat-on%20discord-7289DA.svg" alt="Discord Chat" />
29
+ </a>
30
+ <a href="https://twitter.com/intent/follow?screen_name=medusajs">
31
+ <img src="https://img.shields.io/twitter/follow/medusajs.svg?label=Follow%20@medusajs" alt="Follow @medusajs" />
32
+ </a>
33
+ </p>
34
+
35
+ ## Compatibility
36
+
37
+ This starter is compatible with versions >= 2.4.0 of `@medusajs/medusa`.
38
+
39
+ ## Getting Started
40
+
41
+ Visit the [Quickstart Guide](https://docs.medusajs.com/learn/installation) to set up a server.
42
+
43
+ Visit the [Plugins documentation](https://docs.medusajs.com/learn/fundamentals/plugins) to learn more about plugins and how to create them.
44
+
45
+ Visit the [Docs](https://docs.medusajs.com/learn/installation#get-started) to learn more about our system requirements.
46
+
47
+ ## Register the plugin (required for Analytics in Extensions)
48
+
49
+ For the **Analytics** item (and **Orders Analytics**) to appear under **Extensions** in the Medusa admin sidebar, add this plugin to your Medusa app's config and follow the steps below.
50
+
51
+ ### 1. Add to `medusa-config.ts` (or `medusa-config.js`)
52
+
53
+ **Option A — Package name** (if `medusa-analytics` is in your app's `node_modules`):
54
+
55
+ ```ts
56
+ plugins: [
57
+ // ... other plugins
58
+ {
59
+ resolve: "medusa-analytics",
60
+ options: {},
61
+ },
62
+ ]
63
+ ```
64
+
65
+ **Option B — Resolve to package root** (recommended; works with local link or npm install):
66
+
67
+ ```ts
68
+ import path from "path"
69
+
70
+ // In plugins array:
71
+ {
72
+ resolve: path.dirname(require.resolve("medusa-analytics/package.json")),
73
+ options: {},
74
+ },
75
+ ```
76
+
77
+ This makes Medusa use the directory that contains `package.json`, so the server starts and the admin can load the plugin's extensions.
78
+
79
+ ### 2. Install the plugin in your Medusa app
80
+
81
+ From your **Medusa app** directory (e.g. `my-medusa-store`):
82
+
83
+ ```bash
84
+ npm install /path/to/medusa-plugins/medusa-analytics
85
+ # or, if published: npm install medusa-analytics
86
+ ```
87
+
88
+ ### 3. Build the plugin
89
+
90
+ From the **medusa-analytics** package directory:
91
+
92
+ ```bash
93
+ npx medusa plugin:build
94
+ ```
95
+
96
+ ### 4. Start (or restart) the full app
97
+
98
+ From your **Medusa app** directory:
99
+
100
+ ```bash
101
+ npx medusa develop
102
+ ```
103
+
104
+ Then open the admin in the browser, hard-refresh (Ctrl+Shift+R or Cmd+Shift+R), and check **Extensions** in the sidebar for **Analytics** and **Orders Analytics**.
105
+
106
+ ### Analytics still not showing in Extensions
107
+
108
+ The dashboard loads extensions by **package name** from `node_modules`. Using a path in `resolve` (e.g. `path.dirname(require.resolve("medusa-analytics/package.json"))`) can fix server startup but **Analytics may still not show** because the dashboard needs the name `"medusa-analytics"` to load the admin bundle.
109
+
110
+ **Fix:**
111
+
112
+ 1. **Resolve path**
113
+ `resolve` must point to the **package root** (the folder that contains this plugin’s `package.json` with the `"./admin"` export).
114
+ - If using a local path, use an absolute path so it works from the app’s run directory, e.g.
115
+ `resolve: require.resolve("../path/to/medusa-analytics")`
116
+ (path relative to your `medusa-config` file or project root).
117
+
118
+ 2. **Build the plugin**
119
+ From this repo’s `medusa-analytics` directory run:
120
+ `npx medusa plugin:build`
121
+ so that `.medusa/server/src/admin/index.js` (and the rest of `.medusa/server`) exists and is up to date.
122
+
123
+ 3. **Install in the app**
124
+ If you use `resolve: "medusa-analytics"` (package name), the app must have this package installed (e.g. `npm install <path-to-medusa-analytics>` or from npm). The admin is loaded from `medusa-analytics/admin`, which resolves via the package’s `exports` to `.medusa/server/src/admin/index.js`.
125
+
126
+ 4. **Restart and cache**
127
+ Restart the Medusa backend after changing config or rebuilding the plugin. Then hard-refresh the admin (or clear site data for the admin origin) so the new extensions are loaded.
128
+
129
+ 5. **Check for load errors**
130
+ In the browser: DevTools → Console for errors when the admin loads.
131
+ On the server: startup logs for any error when loading `medusa-analytics` or `medusa-analytics/admin`.
132
+ If the admin module fails to load (e.g. missing dependency or wrong path), that plugin’s menu items will not appear.
133
+
134
+ ## What is Medusa
135
+
136
+ Medusa is a set of commerce modules and tools that allow you to build rich, reliable, and performant commerce applications without reinventing core commerce logic. The modules can be customized and used to build advanced ecommerce stores, marketplaces, or any product that needs foundational commerce primitives. All modules are open-source and freely available on npm.
137
+
138
+ Learn more about [Medusa’s architecture](https://docs.medusajs.com/learn/introduction/architecture) and [commerce modules](https://docs.medusajs.com/learn/fundamentals/modules/commerce-modules) in the Docs.
139
+
140
+ ## Community & Contributions
141
+
142
+ The community and core team are available in [GitHub Discussions](https://github.com/medusajs/medusa/discussions), where you can ask for support, discuss roadmap, and share ideas.
143
+
144
+ Join our [Discord server](https://discord.com/invite/medusajs) to meet other community members.
145
+
146
+ ## Other channels
147
+
148
+ - [GitHub Issues](https://github.com/medusajs/medusa/issues)
149
+ - [Twitter](https://twitter.com/medusajs)
150
+ - [LinkedIn](https://www.linkedin.com/company/medusajs)
151
+ - [Medusa Blog](https://medusajs.com/blog/)
package/index.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Plugin entry at package root so Medusa resolves to a directory that contains package.json.
3
+ * Re-exports the built server entry.
4
+ */
5
+ module.exports = require("./.medusa/server/src/index.js");
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "medusa-analytics",
3
+ "version": "0.0.1",
4
+ "description": "A starter for Medusa plugins.",
5
+ "author": "Medusa (https://medusajs.com)",
6
+ "license": "MIT",
7
+ "main": "./index.js",
8
+ "files": [
9
+ "index.js",
10
+ ".medusa/server"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "import": "./.medusa/server/src/index.js",
15
+ "require": "./index.js",
16
+ "default": "./index.js"
17
+ },
18
+ "./package.json": "./package.json",
19
+ "./workflows": "./.medusa/server/src/workflows/index.js",
20
+ "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js",
21
+ "./modules/*": "./.medusa/server/src/modules/*/index.js",
22
+ "./providers/*": "./.medusa/server/src/providers/*/index.js",
23
+ "./*": "./.medusa/server/src/*.js",
24
+ "./admin": {
25
+ "import": "./.medusa/server/src/admin/index.mjs",
26
+ "require": "./.medusa/server/src/admin/index.js",
27
+ "default": "./.medusa/server/src/admin/index.js"
28
+ }
29
+ },
30
+ "keywords": [
31
+ "medusa",
32
+ "plugin",
33
+ "medusa-plugin-other",
34
+ "medusa-plugin",
35
+ "medusa-v2"
36
+ ],
37
+ "scripts": {
38
+ "build": "medusa plugin:build",
39
+ "dev": "medusa plugin:develop",
40
+ "prepublishOnly": "medusa plugin:build",
41
+ "push-to-app": "node scripts/push-build-to-app.js"
42
+ },
43
+ "devDependencies": {
44
+ "@medusajs/admin-sdk": "2.12.4",
45
+ "@medusajs/cli": "2.12.4",
46
+ "@medusajs/framework": "2.12.4",
47
+ "@medusajs/icons": "2.12.4",
48
+ "@medusajs/medusa": "2.12.4",
49
+ "@medusajs/test-utils": "2.12.4",
50
+ "@medusajs/ui": "4.0.25",
51
+ "@swc/core": "^1.7.28",
52
+ "@types/node": "^20.0.0",
53
+ "@types/react": "^18.3.2",
54
+ "@types/react-dom": "^18.2.25",
55
+ "prop-types": "^15.8.1",
56
+ "react": "^18.2.0",
57
+ "react-dom": "^18.2.0",
58
+ "ts-node": "^10.9.2",
59
+ "typescript": "^5.6.2",
60
+ "vite": "^5.2.11",
61
+ "yalc": "^1.0.0-pre.53"
62
+ },
63
+ "peerDependencies": {
64
+ "@medusajs/admin-sdk": "2.12.4",
65
+ "@medusajs/cli": "2.12.4",
66
+ "@medusajs/framework": "2.12.4",
67
+ "@medusajs/icons": "2.12.4",
68
+ "@medusajs/medusa": "2.12.4",
69
+ "@medusajs/test-utils": "2.12.4",
70
+ "@medusajs/ui": "4.0.25"
71
+ },
72
+ "engines": {
73
+ "node": ">=20"
74
+ },
75
+ "dependencies": {
76
+ "recharts": "^3.7.0"
77
+ }
78
+ }