@alphabite/medusa-wishlist 0.6.1 → 0.7.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.
- package/.medusa/server/src/admin/index.js +219 -103
- package/.medusa/server/src/admin/index.mjs +221 -105
- package/.medusa/server/src/api/admin/wishlists/analytics/route.js +67 -0
- package/.medusa/server/src/api/admin/wishlists/analytics/types.js +3 -0
- package/.medusa/server/src/api/admin/wishlists/analytics/validators.js +10 -0
- package/.medusa/server/src/modules/wishlist/service.js +175 -1
- package/.medusa/server/src/modules/wishlist/types/analytics.js +3 -0
- package/.medusa/types/modules/wishlist/service.d.ts +2 -0
- package/.medusa/types/modules/wishlist/types/analytics.d.ts +45 -0
- package/README.md +14 -0
- package/package.json +1 -1
|
@@ -28,102 +28,220 @@ const ProductWidget = ({ data: product }) => {
|
|
|
28
28
|
adminSdk.defineWidgetConfig({
|
|
29
29
|
zone: "product.details.before"
|
|
30
30
|
});
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
const RANGE_PRESETS = {
|
|
32
|
+
"7": 7,
|
|
33
|
+
"30": 30,
|
|
34
|
+
"90": 90
|
|
35
|
+
};
|
|
36
|
+
const KpiCard = ({
|
|
37
|
+
label,
|
|
38
|
+
value,
|
|
39
|
+
delta
|
|
40
|
+
}) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1 rounded-lg border p-4", children: [
|
|
41
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: label }),
|
|
42
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [
|
|
43
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xlarge", weight: "plus", children: value }),
|
|
44
|
+
delta !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
45
|
+
ui.Badge,
|
|
46
|
+
{
|
|
47
|
+
size: "2xsmall",
|
|
48
|
+
color: delta > 0 ? "green" : delta < 0 ? "red" : "grey",
|
|
49
|
+
children: [
|
|
50
|
+
delta > 0 ? "+" : "",
|
|
51
|
+
delta,
|
|
52
|
+
"%"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
] })
|
|
57
|
+
] });
|
|
58
|
+
const WishlistAnalyticsTab = () => {
|
|
59
|
+
var _a, _b;
|
|
60
|
+
const [rangeDays, setRangeDays] = react.useState("30");
|
|
61
|
+
const [channelId, setChannelId] = react.useState("all");
|
|
62
|
+
const { data: channelsRes } = reactQuery.useQuery({
|
|
63
|
+
queryKey: ["wishlist", "analytics", "channels"],
|
|
64
|
+
queryFn: () => sdk.admin.salesChannel.list({ limit: 100 })
|
|
39
65
|
});
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
ui.toast.success("Wishlist settings updated");
|
|
57
|
-
},
|
|
58
|
-
onError: (err) => {
|
|
59
|
-
const msg = err instanceof Error ? err.message : "Failed to update";
|
|
60
|
-
ui.toast.error(msg);
|
|
66
|
+
const range = react.useMemo(() => {
|
|
67
|
+
const to = /* @__PURE__ */ new Date();
|
|
68
|
+
const from = new Date(
|
|
69
|
+
to.getTime() - RANGE_PRESETS[rangeDays] * 24 * 60 * 60 * 1e3
|
|
70
|
+
);
|
|
71
|
+
return { from: from.toISOString(), to: to.toISOString() };
|
|
72
|
+
}, [rangeDays]);
|
|
73
|
+
const { data, isLoading, isError } = reactQuery.useQuery({
|
|
74
|
+
queryKey: ["wishlist", "analytics", range, channelId],
|
|
75
|
+
queryFn: () => {
|
|
76
|
+
const params = new URLSearchParams({ from: range.from, to: range.to });
|
|
77
|
+
if (channelId !== "all") params.set("sales_channel_id", channelId);
|
|
78
|
+
return sdk.client.fetch(
|
|
79
|
+
`/admin/wishlists/analytics?${params.toString()}`,
|
|
80
|
+
{ method: "GET" }
|
|
81
|
+
);
|
|
61
82
|
}
|
|
62
83
|
});
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
82
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.
|
|
83
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.
|
|
84
|
-
|
|
84
|
+
const maxTrend = react.useMemo(
|
|
85
|
+
() => Math.max(
|
|
86
|
+
1,
|
|
87
|
+
...(data == null ? void 0 : data.trend.map((t) => Math.max(t.wishlists, t.items))) ?? [1]
|
|
88
|
+
),
|
|
89
|
+
[data]
|
|
90
|
+
);
|
|
91
|
+
if (isError) {
|
|
92
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-6", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", children: "Failed to load analytics." }) });
|
|
93
|
+
}
|
|
94
|
+
if (isLoading || !data) {
|
|
95
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-6", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Loading analytics…" }) });
|
|
96
|
+
}
|
|
97
|
+
const hasData = data.kpis.total_wishlists.value > 0 || data.trend.length > 0 || data.top_products.length > 0;
|
|
98
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-6 px-6 py-6", children: [
|
|
99
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center", children: [
|
|
100
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full sm:w-48", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: rangeDays, onValueChange: setRangeDays, children: [
|
|
101
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
|
|
102
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
|
|
103
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "7", children: "Last 7 days" }),
|
|
104
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "30", children: "Last 30 days" }),
|
|
105
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "90", children: "Last 90 days" })
|
|
106
|
+
] })
|
|
107
|
+
] }) }),
|
|
108
|
+
(((_a = channelsRes == null ? void 0 : channelsRes.sales_channels) == null ? void 0 : _a.length) ?? 0) > 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full sm:w-64", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: channelId, onValueChange: setChannelId, children: [
|
|
109
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "All sales channels" }) }),
|
|
110
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
|
|
111
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "all", children: "All sales channels" }),
|
|
112
|
+
(_b = channelsRes == null ? void 0 : channelsRes.sales_channels) == null ? void 0 : _b.map((c) => /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: c.id, children: c.name }, c.id))
|
|
113
|
+
] })
|
|
114
|
+
] }) })
|
|
115
|
+
] }),
|
|
116
|
+
!hasData ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "No wishlist activity in this period yet." }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
117
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3", children: [
|
|
85
118
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
86
|
-
|
|
119
|
+
KpiCard,
|
|
87
120
|
{
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
121
|
+
label: "Total wishlists",
|
|
122
|
+
value: String(data.kpis.total_wishlists.value),
|
|
123
|
+
delta: data.kpis.total_wishlists.delta_pct
|
|
91
124
|
}
|
|
92
|
-
)
|
|
93
|
-
] }),
|
|
94
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-4", children: [
|
|
95
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
|
|
96
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "allow-multiple", className: "font-medium", children: "Allow multiple wishlists per customer" }),
|
|
97
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Signed-in customers can create more than one wishlist." })
|
|
98
|
-
] }),
|
|
125
|
+
),
|
|
99
126
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
100
|
-
|
|
127
|
+
KpiCard,
|
|
101
128
|
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
129
|
+
label: "Saved items",
|
|
130
|
+
value: String(data.kpis.total_items.value),
|
|
131
|
+
delta: data.kpis.total_items.delta_pct
|
|
132
|
+
}
|
|
133
|
+
),
|
|
134
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
135
|
+
KpiCard,
|
|
136
|
+
{
|
|
137
|
+
label: "Avg items / wishlist",
|
|
138
|
+
value: String(data.kpis.avg_items_per_wishlist.value),
|
|
139
|
+
delta: data.kpis.avg_items_per_wishlist.delta_pct
|
|
140
|
+
}
|
|
141
|
+
),
|
|
142
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
143
|
+
KpiCard,
|
|
144
|
+
{
|
|
145
|
+
label: "Active vs empty",
|
|
146
|
+
value: `${data.kpis.active_wishlists} / ${data.kpis.empty_wishlists}`
|
|
147
|
+
}
|
|
148
|
+
),
|
|
149
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
150
|
+
KpiCard,
|
|
151
|
+
{
|
|
152
|
+
label: "Guest vs registered",
|
|
153
|
+
value: `${data.kpis.guest_wishlists} / ${data.kpis.registered_wishlists}`
|
|
154
|
+
}
|
|
155
|
+
),
|
|
156
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
157
|
+
KpiCard,
|
|
158
|
+
{
|
|
159
|
+
label: "Unique customers",
|
|
160
|
+
value: String(data.kpis.unique_customers.value),
|
|
161
|
+
delta: data.kpis.unique_customers.delta_pct
|
|
105
162
|
}
|
|
106
163
|
)
|
|
107
164
|
] }),
|
|
108
|
-
/* @__PURE__ */ jsxRuntime.
|
|
109
|
-
ui.
|
|
110
|
-
{
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
165
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 rounded-lg border p-4", children: [
|
|
166
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "Activity over time" }),
|
|
167
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-40 items-end gap-1 overflow-x-auto", children: data.trend.map((t) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
168
|
+
"div",
|
|
169
|
+
{
|
|
170
|
+
className: "flex min-w-[10px] flex-1 flex-col items-center justify-end gap-0.5",
|
|
171
|
+
title: `${t.date}: ${t.wishlists} wishlists, ${t.items} items`,
|
|
172
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-32 w-full items-end justify-center gap-0.5", children: [
|
|
173
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
174
|
+
"div",
|
|
175
|
+
{
|
|
176
|
+
className: "w-1/2 rounded-t bg-ui-fg-interactive",
|
|
177
|
+
style: {
|
|
178
|
+
height: `${t.wishlists / maxTrend * 100}%`,
|
|
179
|
+
minHeight: t.wishlists > 0 ? "2px" : void 0
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
),
|
|
183
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
184
|
+
"div",
|
|
185
|
+
{
|
|
186
|
+
className: "w-1/2 rounded-t bg-ui-fg-muted",
|
|
187
|
+
style: {
|
|
188
|
+
height: `${t.items / maxTrend * 100}%`,
|
|
189
|
+
minHeight: t.items > 0 ? "2px" : void 0
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
] })
|
|
194
|
+
},
|
|
195
|
+
t.date
|
|
196
|
+
)) }),
|
|
197
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-4", children: [
|
|
198
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: "▮ Wishlists" }),
|
|
199
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: "▮ Items" })
|
|
200
|
+
] })
|
|
201
|
+
] }),
|
|
202
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
203
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Most-wishlisted products (top 10)" }),
|
|
204
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
205
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
206
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Product" }),
|
|
207
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: "Wishlists" }),
|
|
208
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: "Items" })
|
|
209
|
+
] }) }),
|
|
210
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.top_products.map((p) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
211
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
212
|
+
p.thumbnail ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
213
|
+
"img",
|
|
214
|
+
{
|
|
215
|
+
src: p.thumbnail,
|
|
216
|
+
alt: "",
|
|
217
|
+
className: "h-8 w-8 rounded object-cover"
|
|
218
|
+
}
|
|
219
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-8 w-8 rounded bg-ui-bg-subtle" }),
|
|
220
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: p.title })
|
|
221
|
+
] }) }),
|
|
222
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: p.wishlist_count }),
|
|
223
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: p.item_count })
|
|
224
|
+
] }, p.product_id)) })
|
|
225
|
+
] })
|
|
226
|
+
] }),
|
|
227
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
228
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Most-wishlisted variants (top 10)" }),
|
|
229
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
230
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
231
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Variant" }),
|
|
232
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: "Wishlists" })
|
|
233
|
+
] }) }),
|
|
234
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.top_variants.map((v) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
235
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: v.title }) }),
|
|
236
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: v.wishlist_count })
|
|
237
|
+
] }, v.product_variant_id)) })
|
|
238
|
+
] })
|
|
239
|
+
] })
|
|
240
|
+
] })
|
|
119
241
|
] });
|
|
120
242
|
};
|
|
121
|
-
const config$1 = adminSdk.defineRouteConfig({
|
|
122
|
-
label: "Wishlists",
|
|
123
|
-
icon: icons.Heart
|
|
124
|
-
});
|
|
125
243
|
const QUERY_KEY = ["wishlist", "settings"];
|
|
126
|
-
const
|
|
244
|
+
const WishlistSettingsTab = () => {
|
|
127
245
|
const queryClient = reactQuery.useQueryClient();
|
|
128
246
|
const { data, isLoading } = reactQuery.useQuery({
|
|
129
247
|
queryKey: QUERY_KEY,
|
|
@@ -165,12 +283,9 @@ const WishlistSettingsPage = () => {
|
|
|
165
283
|
}
|
|
166
284
|
update.mutate(patch);
|
|
167
285
|
};
|
|
168
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
169
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "
|
|
170
|
-
|
|
171
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Control who can use wishlists in your storefront." })
|
|
172
|
-
] }) }),
|
|
173
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-6 px-6 py-6", children: isLoading ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Loading…" }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
286
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-6 px-6 py-6", children: [
|
|
287
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Control who can use wishlists in your storefront." }) }),
|
|
288
|
+
isLoading ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Loading…" }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
174
289
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-4", children: [
|
|
175
290
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
|
|
176
291
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "allow-guest", className: "font-medium", children: "Allow guest wishlists" }),
|
|
@@ -209,9 +324,22 @@ const WishlistSettingsPage = () => {
|
|
|
209
324
|
children: "Save"
|
|
210
325
|
}
|
|
211
326
|
) })
|
|
212
|
-
] })
|
|
327
|
+
] })
|
|
213
328
|
] });
|
|
214
329
|
};
|
|
330
|
+
const WishlistPage = () => {
|
|
331
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "p-0", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Tabs, { defaultValue: "analytics", children: [
|
|
332
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 border-b px-6 py-4", children: [
|
|
333
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Wishlists" }),
|
|
334
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Tabs.List, { children: [
|
|
335
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Tabs.Trigger, { value: "analytics", children: "Analytics" }),
|
|
336
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Tabs.Trigger, { value: "settings", children: "Settings" })
|
|
337
|
+
] })
|
|
338
|
+
] }),
|
|
339
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Tabs.Content, { value: "analytics", children: /* @__PURE__ */ jsxRuntime.jsx(WishlistAnalyticsTab, {}) }),
|
|
340
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Tabs.Content, { value: "settings", children: /* @__PURE__ */ jsxRuntime.jsx(WishlistSettingsTab, {}) })
|
|
341
|
+
] }) });
|
|
342
|
+
};
|
|
215
343
|
const config = adminSdk.defineRouteConfig({
|
|
216
344
|
label: "Wishlists",
|
|
217
345
|
icon: icons.Heart
|
|
@@ -225,29 +353,17 @@ const widgetModule = { widgets: [
|
|
|
225
353
|
const routeModule = {
|
|
226
354
|
routes: [
|
|
227
355
|
{
|
|
228
|
-
Component:
|
|
356
|
+
Component: WishlistPage,
|
|
229
357
|
path: "/wishlists"
|
|
230
|
-
},
|
|
231
|
-
{
|
|
232
|
-
Component: WishlistSettingsPage,
|
|
233
|
-
path: "/settings/wishlists"
|
|
234
358
|
}
|
|
235
359
|
]
|
|
236
360
|
};
|
|
237
361
|
const menuItemModule = {
|
|
238
362
|
menuItems: [
|
|
239
|
-
{
|
|
240
|
-
label: config$1.label,
|
|
241
|
-
icon: config$1.icon,
|
|
242
|
-
path: "/wishlists",
|
|
243
|
-
nested: void 0,
|
|
244
|
-
rank: void 0,
|
|
245
|
-
translationNs: void 0
|
|
246
|
-
},
|
|
247
363
|
{
|
|
248
364
|
label: config.label,
|
|
249
365
|
icon: config.icon,
|
|
250
|
-
path: "/
|
|
366
|
+
path: "/wishlists",
|
|
251
367
|
nested: void 0,
|
|
252
368
|
rank: void 0,
|
|
253
369
|
translationNs: void 0
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { defineWidgetConfig, defineRouteConfig } from "@medusajs/admin-sdk";
|
|
3
|
-
import { Container, Heading, Text, toast, Label, Switch, Button } from "@medusajs/ui";
|
|
3
|
+
import { Container, Heading, Text, Select, Table, Badge, toast, Label, Switch, Button, Tabs } from "@medusajs/ui";
|
|
4
4
|
import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query";
|
|
5
5
|
import Medusa from "@medusajs/js-sdk";
|
|
6
6
|
import { Heart } from "@medusajs/icons";
|
|
7
|
-
import { useState, useEffect } from "react";
|
|
7
|
+
import { useState, useMemo, useEffect } from "react";
|
|
8
8
|
const sdk = new Medusa({
|
|
9
9
|
baseUrl: __BACKEND_URL__ || "http://localhost:9000",
|
|
10
10
|
debug: process.env.NODE_ENV === "development",
|
|
@@ -25,102 +25,220 @@ const ProductWidget = ({ data: product }) => {
|
|
|
25
25
|
defineWidgetConfig({
|
|
26
26
|
zone: "product.details.before"
|
|
27
27
|
});
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
const RANGE_PRESETS = {
|
|
29
|
+
"7": 7,
|
|
30
|
+
"30": 30,
|
|
31
|
+
"90": 90
|
|
32
|
+
};
|
|
33
|
+
const KpiCard = ({
|
|
34
|
+
label,
|
|
35
|
+
value,
|
|
36
|
+
delta
|
|
37
|
+
}) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 rounded-lg border p-4", children: [
|
|
38
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: label }),
|
|
39
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-2", children: [
|
|
40
|
+
/* @__PURE__ */ jsx(Text, { size: "xlarge", weight: "plus", children: value }),
|
|
41
|
+
delta !== void 0 && /* @__PURE__ */ jsxs(
|
|
42
|
+
Badge,
|
|
43
|
+
{
|
|
44
|
+
size: "2xsmall",
|
|
45
|
+
color: delta > 0 ? "green" : delta < 0 ? "red" : "grey",
|
|
46
|
+
children: [
|
|
47
|
+
delta > 0 ? "+" : "",
|
|
48
|
+
delta,
|
|
49
|
+
"%"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
] })
|
|
54
|
+
] });
|
|
55
|
+
const WishlistAnalyticsTab = () => {
|
|
56
|
+
var _a, _b;
|
|
57
|
+
const [rangeDays, setRangeDays] = useState("30");
|
|
58
|
+
const [channelId, setChannelId] = useState("all");
|
|
59
|
+
const { data: channelsRes } = useQuery({
|
|
60
|
+
queryKey: ["wishlist", "analytics", "channels"],
|
|
61
|
+
queryFn: () => sdk.admin.salesChannel.list({ limit: 100 })
|
|
36
62
|
});
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
toast.success("Wishlist settings updated");
|
|
54
|
-
},
|
|
55
|
-
onError: (err) => {
|
|
56
|
-
const msg = err instanceof Error ? err.message : "Failed to update";
|
|
57
|
-
toast.error(msg);
|
|
63
|
+
const range = useMemo(() => {
|
|
64
|
+
const to = /* @__PURE__ */ new Date();
|
|
65
|
+
const from = new Date(
|
|
66
|
+
to.getTime() - RANGE_PRESETS[rangeDays] * 24 * 60 * 60 * 1e3
|
|
67
|
+
);
|
|
68
|
+
return { from: from.toISOString(), to: to.toISOString() };
|
|
69
|
+
}, [rangeDays]);
|
|
70
|
+
const { data, isLoading, isError } = useQuery({
|
|
71
|
+
queryKey: ["wishlist", "analytics", range, channelId],
|
|
72
|
+
queryFn: () => {
|
|
73
|
+
const params = new URLSearchParams({ from: range.from, to: range.to });
|
|
74
|
+
if (channelId !== "all") params.set("sales_channel_id", channelId);
|
|
75
|
+
return sdk.client.fetch(
|
|
76
|
+
`/admin/wishlists/analytics?${params.toString()}`,
|
|
77
|
+
{ method: "GET" }
|
|
78
|
+
);
|
|
58
79
|
}
|
|
59
80
|
});
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
/* @__PURE__ */ jsxs(
|
|
79
|
-
/* @__PURE__ */ jsx(
|
|
80
|
-
/* @__PURE__ */ jsx(
|
|
81
|
-
|
|
81
|
+
const maxTrend = useMemo(
|
|
82
|
+
() => Math.max(
|
|
83
|
+
1,
|
|
84
|
+
...(data == null ? void 0 : data.trend.map((t) => Math.max(t.wishlists, t.items))) ?? [1]
|
|
85
|
+
),
|
|
86
|
+
[data]
|
|
87
|
+
);
|
|
88
|
+
if (isError) {
|
|
89
|
+
return /* @__PURE__ */ jsx("div", { className: "px-6 py-6", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error", children: "Failed to load analytics." }) });
|
|
90
|
+
}
|
|
91
|
+
if (isLoading || !data) {
|
|
92
|
+
return /* @__PURE__ */ jsx("div", { className: "px-6 py-6", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: "Loading analytics…" }) });
|
|
93
|
+
}
|
|
94
|
+
const hasData = data.kpis.total_wishlists.value > 0 || data.trend.length > 0 || data.top_products.length > 0;
|
|
95
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-6 px-6 py-6", children: [
|
|
96
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center", children: [
|
|
97
|
+
/* @__PURE__ */ jsx("div", { className: "w-full sm:w-48", children: /* @__PURE__ */ jsxs(Select, { value: rangeDays, onValueChange: setRangeDays, children: [
|
|
98
|
+
/* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, {}) }),
|
|
99
|
+
/* @__PURE__ */ jsxs(Select.Content, { children: [
|
|
100
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "7", children: "Last 7 days" }),
|
|
101
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "30", children: "Last 30 days" }),
|
|
102
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "90", children: "Last 90 days" })
|
|
103
|
+
] })
|
|
104
|
+
] }) }),
|
|
105
|
+
(((_a = channelsRes == null ? void 0 : channelsRes.sales_channels) == null ? void 0 : _a.length) ?? 0) > 1 && /* @__PURE__ */ jsx("div", { className: "w-full sm:w-64", children: /* @__PURE__ */ jsxs(Select, { value: channelId, onValueChange: setChannelId, children: [
|
|
106
|
+
/* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "All sales channels" }) }),
|
|
107
|
+
/* @__PURE__ */ jsxs(Select.Content, { children: [
|
|
108
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "all", children: "All sales channels" }),
|
|
109
|
+
(_b = channelsRes == null ? void 0 : channelsRes.sales_channels) == null ? void 0 : _b.map((c) => /* @__PURE__ */ jsx(Select.Item, { value: c.id, children: c.name }, c.id))
|
|
110
|
+
] })
|
|
111
|
+
] }) })
|
|
112
|
+
] }),
|
|
113
|
+
!hasData ? /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: "No wishlist activity in this period yet." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
114
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3", children: [
|
|
82
115
|
/* @__PURE__ */ jsx(
|
|
83
|
-
|
|
116
|
+
KpiCard,
|
|
84
117
|
{
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
118
|
+
label: "Total wishlists",
|
|
119
|
+
value: String(data.kpis.total_wishlists.value),
|
|
120
|
+
delta: data.kpis.total_wishlists.delta_pct
|
|
88
121
|
}
|
|
89
|
-
)
|
|
90
|
-
] }),
|
|
91
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4", children: [
|
|
92
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
93
|
-
/* @__PURE__ */ jsx(Label, { htmlFor: "allow-multiple", className: "font-medium", children: "Allow multiple wishlists per customer" }),
|
|
94
|
-
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Signed-in customers can create more than one wishlist." })
|
|
95
|
-
] }),
|
|
122
|
+
),
|
|
96
123
|
/* @__PURE__ */ jsx(
|
|
97
|
-
|
|
124
|
+
KpiCard,
|
|
98
125
|
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
126
|
+
label: "Saved items",
|
|
127
|
+
value: String(data.kpis.total_items.value),
|
|
128
|
+
delta: data.kpis.total_items.delta_pct
|
|
129
|
+
}
|
|
130
|
+
),
|
|
131
|
+
/* @__PURE__ */ jsx(
|
|
132
|
+
KpiCard,
|
|
133
|
+
{
|
|
134
|
+
label: "Avg items / wishlist",
|
|
135
|
+
value: String(data.kpis.avg_items_per_wishlist.value),
|
|
136
|
+
delta: data.kpis.avg_items_per_wishlist.delta_pct
|
|
137
|
+
}
|
|
138
|
+
),
|
|
139
|
+
/* @__PURE__ */ jsx(
|
|
140
|
+
KpiCard,
|
|
141
|
+
{
|
|
142
|
+
label: "Active vs empty",
|
|
143
|
+
value: `${data.kpis.active_wishlists} / ${data.kpis.empty_wishlists}`
|
|
144
|
+
}
|
|
145
|
+
),
|
|
146
|
+
/* @__PURE__ */ jsx(
|
|
147
|
+
KpiCard,
|
|
148
|
+
{
|
|
149
|
+
label: "Guest vs registered",
|
|
150
|
+
value: `${data.kpis.guest_wishlists} / ${data.kpis.registered_wishlists}`
|
|
151
|
+
}
|
|
152
|
+
),
|
|
153
|
+
/* @__PURE__ */ jsx(
|
|
154
|
+
KpiCard,
|
|
155
|
+
{
|
|
156
|
+
label: "Unique customers",
|
|
157
|
+
value: String(data.kpis.unique_customers.value),
|
|
158
|
+
delta: data.kpis.unique_customers.delta_pct
|
|
102
159
|
}
|
|
103
160
|
)
|
|
104
161
|
] }),
|
|
105
|
-
/* @__PURE__ */
|
|
106
|
-
|
|
107
|
-
{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
162
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 rounded-lg border p-4", children: [
|
|
163
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "Activity over time" }),
|
|
164
|
+
/* @__PURE__ */ jsx("div", { className: "flex h-40 items-end gap-1 overflow-x-auto", children: data.trend.map((t) => /* @__PURE__ */ jsx(
|
|
165
|
+
"div",
|
|
166
|
+
{
|
|
167
|
+
className: "flex min-w-[10px] flex-1 flex-col items-center justify-end gap-0.5",
|
|
168
|
+
title: `${t.date}: ${t.wishlists} wishlists, ${t.items} items`,
|
|
169
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex h-32 w-full items-end justify-center gap-0.5", children: [
|
|
170
|
+
/* @__PURE__ */ jsx(
|
|
171
|
+
"div",
|
|
172
|
+
{
|
|
173
|
+
className: "w-1/2 rounded-t bg-ui-fg-interactive",
|
|
174
|
+
style: {
|
|
175
|
+
height: `${t.wishlists / maxTrend * 100}%`,
|
|
176
|
+
minHeight: t.wishlists > 0 ? "2px" : void 0
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
),
|
|
180
|
+
/* @__PURE__ */ jsx(
|
|
181
|
+
"div",
|
|
182
|
+
{
|
|
183
|
+
className: "w-1/2 rounded-t bg-ui-fg-muted",
|
|
184
|
+
style: {
|
|
185
|
+
height: `${t.items / maxTrend * 100}%`,
|
|
186
|
+
minHeight: t.items > 0 ? "2px" : void 0
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
] })
|
|
191
|
+
},
|
|
192
|
+
t.date
|
|
193
|
+
)) }),
|
|
194
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-4", children: [
|
|
195
|
+
/* @__PURE__ */ jsx(Text, { size: "xsmall", className: "text-ui-fg-subtle", children: "▮ Wishlists" }),
|
|
196
|
+
/* @__PURE__ */ jsx(Text, { size: "xsmall", className: "text-ui-fg-muted", children: "▮ Items" })
|
|
197
|
+
] })
|
|
198
|
+
] }),
|
|
199
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
200
|
+
/* @__PURE__ */ jsx(Heading, { level: "h3", children: "Most-wishlisted products (top 10)" }),
|
|
201
|
+
/* @__PURE__ */ jsxs(Table, { children: [
|
|
202
|
+
/* @__PURE__ */ jsx(Table.Header, { children: /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
203
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Product" }),
|
|
204
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { className: "text-right", children: "Wishlists" }),
|
|
205
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { className: "text-right", children: "Items" })
|
|
206
|
+
] }) }),
|
|
207
|
+
/* @__PURE__ */ jsx(Table.Body, { children: data.top_products.map((p) => /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
208
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
209
|
+
p.thumbnail ? /* @__PURE__ */ jsx(
|
|
210
|
+
"img",
|
|
211
|
+
{
|
|
212
|
+
src: p.thumbnail,
|
|
213
|
+
alt: "",
|
|
214
|
+
className: "h-8 w-8 rounded object-cover"
|
|
215
|
+
}
|
|
216
|
+
) : /* @__PURE__ */ jsx("div", { className: "h-8 w-8 rounded bg-ui-bg-subtle" }),
|
|
217
|
+
/* @__PURE__ */ jsx(Text, { size: "small", children: p.title })
|
|
218
|
+
] }) }),
|
|
219
|
+
/* @__PURE__ */ jsx(Table.Cell, { className: "text-right", children: p.wishlist_count }),
|
|
220
|
+
/* @__PURE__ */ jsx(Table.Cell, { className: "text-right", children: p.item_count })
|
|
221
|
+
] }, p.product_id)) })
|
|
222
|
+
] })
|
|
223
|
+
] }),
|
|
224
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
225
|
+
/* @__PURE__ */ jsx(Heading, { level: "h3", children: "Most-wishlisted variants (top 10)" }),
|
|
226
|
+
/* @__PURE__ */ jsxs(Table, { children: [
|
|
227
|
+
/* @__PURE__ */ jsx(Table.Header, { children: /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
228
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Variant" }),
|
|
229
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { className: "text-right", children: "Wishlists" })
|
|
230
|
+
] }) }),
|
|
231
|
+
/* @__PURE__ */ jsx(Table.Body, { children: data.top_variants.map((v) => /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
232
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(Text, { size: "small", children: v.title }) }),
|
|
233
|
+
/* @__PURE__ */ jsx(Table.Cell, { className: "text-right", children: v.wishlist_count })
|
|
234
|
+
] }, v.product_variant_id)) })
|
|
235
|
+
] })
|
|
236
|
+
] })
|
|
237
|
+
] })
|
|
116
238
|
] });
|
|
117
239
|
};
|
|
118
|
-
const config$1 = defineRouteConfig({
|
|
119
|
-
label: "Wishlists",
|
|
120
|
-
icon: Heart
|
|
121
|
-
});
|
|
122
240
|
const QUERY_KEY = ["wishlist", "settings"];
|
|
123
|
-
const
|
|
241
|
+
const WishlistSettingsTab = () => {
|
|
124
242
|
const queryClient = useQueryClient();
|
|
125
243
|
const { data, isLoading } = useQuery({
|
|
126
244
|
queryKey: QUERY_KEY,
|
|
@@ -162,12 +280,9 @@ const WishlistSettingsPage = () => {
|
|
|
162
280
|
}
|
|
163
281
|
update.mutate(patch);
|
|
164
282
|
};
|
|
165
|
-
return /* @__PURE__ */ jsxs(
|
|
166
|
-
/* @__PURE__ */ jsx("div", { className: "
|
|
167
|
-
|
|
168
|
-
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: "Control who can use wishlists in your storefront." })
|
|
169
|
-
] }) }),
|
|
170
|
-
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-6 px-6 py-6", children: isLoading ? /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: "Loading…" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
283
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-6 px-6 py-6", children: [
|
|
284
|
+
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: "Control who can use wishlists in your storefront." }) }),
|
|
285
|
+
isLoading ? /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: "Loading…" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
171
286
|
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4", children: [
|
|
172
287
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
173
288
|
/* @__PURE__ */ jsx(Label, { htmlFor: "allow-guest", className: "font-medium", children: "Allow guest wishlists" }),
|
|
@@ -206,9 +321,22 @@ const WishlistSettingsPage = () => {
|
|
|
206
321
|
children: "Save"
|
|
207
322
|
}
|
|
208
323
|
) })
|
|
209
|
-
] })
|
|
324
|
+
] })
|
|
210
325
|
] });
|
|
211
326
|
};
|
|
327
|
+
const WishlistPage = () => {
|
|
328
|
+
return /* @__PURE__ */ jsx(Container, { className: "p-0", children: /* @__PURE__ */ jsxs(Tabs, { defaultValue: "analytics", children: [
|
|
329
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 border-b px-6 py-4", children: [
|
|
330
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Wishlists" }),
|
|
331
|
+
/* @__PURE__ */ jsxs(Tabs.List, { children: [
|
|
332
|
+
/* @__PURE__ */ jsx(Tabs.Trigger, { value: "analytics", children: "Analytics" }),
|
|
333
|
+
/* @__PURE__ */ jsx(Tabs.Trigger, { value: "settings", children: "Settings" })
|
|
334
|
+
] })
|
|
335
|
+
] }),
|
|
336
|
+
/* @__PURE__ */ jsx(Tabs.Content, { value: "analytics", children: /* @__PURE__ */ jsx(WishlistAnalyticsTab, {}) }),
|
|
337
|
+
/* @__PURE__ */ jsx(Tabs.Content, { value: "settings", children: /* @__PURE__ */ jsx(WishlistSettingsTab, {}) })
|
|
338
|
+
] }) });
|
|
339
|
+
};
|
|
212
340
|
const config = defineRouteConfig({
|
|
213
341
|
label: "Wishlists",
|
|
214
342
|
icon: Heart
|
|
@@ -222,29 +350,17 @@ const widgetModule = { widgets: [
|
|
|
222
350
|
const routeModule = {
|
|
223
351
|
routes: [
|
|
224
352
|
{
|
|
225
|
-
Component:
|
|
353
|
+
Component: WishlistPage,
|
|
226
354
|
path: "/wishlists"
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
Component: WishlistSettingsPage,
|
|
230
|
-
path: "/settings/wishlists"
|
|
231
355
|
}
|
|
232
356
|
]
|
|
233
357
|
};
|
|
234
358
|
const menuItemModule = {
|
|
235
359
|
menuItems: [
|
|
236
|
-
{
|
|
237
|
-
label: config$1.label,
|
|
238
|
-
icon: config$1.icon,
|
|
239
|
-
path: "/wishlists",
|
|
240
|
-
nested: void 0,
|
|
241
|
-
rank: void 0,
|
|
242
|
-
translationNs: void 0
|
|
243
|
-
},
|
|
244
360
|
{
|
|
245
361
|
label: config.label,
|
|
246
362
|
icon: config.icon,
|
|
247
|
-
path: "/
|
|
363
|
+
path: "/wishlists",
|
|
248
364
|
nested: void 0,
|
|
249
365
|
rank: void 0,
|
|
250
366
|
translationNs: void 0
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GET = GET;
|
|
4
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
5
|
+
const wishlist_1 = require("../../../../modules/wishlist");
|
|
6
|
+
const validators_1 = require("./validators");
|
|
7
|
+
async function GET(req, res) {
|
|
8
|
+
const logger = req.scope.resolve(utils_1.ContainerRegistrationKeys.LOGGER);
|
|
9
|
+
const query = req.scope.resolve(utils_1.ContainerRegistrationKeys.QUERY);
|
|
10
|
+
const service = req.scope.resolve(wishlist_1.WISHLIST_MODULE);
|
|
11
|
+
const parsed = validators_1.WishlistAnalyticsQuerySchema.safeParse(req.query);
|
|
12
|
+
if (!parsed.success) {
|
|
13
|
+
return res.status(400).json({
|
|
14
|
+
error: "Invalid analytics query",
|
|
15
|
+
issues: parsed.error.flatten(),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const view = await service.getAnalytics(parsed.data);
|
|
20
|
+
const productIds = view.top_products.map((p) => p.product_id);
|
|
21
|
+
const variantIds = view.top_variants.map((v) => v.product_variant_id);
|
|
22
|
+
const [products, variants] = await Promise.all([
|
|
23
|
+
productIds.length
|
|
24
|
+
? query.graph({
|
|
25
|
+
entity: "product",
|
|
26
|
+
fields: ["id", "title", "thumbnail"],
|
|
27
|
+
filters: { id: productIds },
|
|
28
|
+
})
|
|
29
|
+
: Promise.resolve({ data: [] }),
|
|
30
|
+
variantIds.length
|
|
31
|
+
? query.graph({
|
|
32
|
+
entity: "product_variant",
|
|
33
|
+
fields: ["id", "title"],
|
|
34
|
+
filters: { id: variantIds },
|
|
35
|
+
})
|
|
36
|
+
: Promise.resolve({ data: [] }),
|
|
37
|
+
]);
|
|
38
|
+
const productRows = products.data;
|
|
39
|
+
const variantRows = variants.data;
|
|
40
|
+
const productMap = new Map(productRows.map((p) => [p.id, p]));
|
|
41
|
+
const variantMap = new Map(variantRows.map((v) => [v.id, v]));
|
|
42
|
+
const response = {
|
|
43
|
+
...view,
|
|
44
|
+
top_products: view.top_products.map((p) => {
|
|
45
|
+
const product = productMap.get(p.product_id);
|
|
46
|
+
return {
|
|
47
|
+
...p,
|
|
48
|
+
title: product?.title ?? p.product_id,
|
|
49
|
+
thumbnail: product?.thumbnail ?? null,
|
|
50
|
+
};
|
|
51
|
+
}),
|
|
52
|
+
top_variants: view.top_variants.map((v) => ({
|
|
53
|
+
...v,
|
|
54
|
+
title: variantMap.get(v.product_variant_id)?.title ?? v.product_variant_id,
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
return res.status(200).json(response);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
logger.error("[admin/wishlists/analytics] GET error:", error);
|
|
61
|
+
return res.status(500).json({
|
|
62
|
+
error: "Failed to load wishlist analytics",
|
|
63
|
+
details: error instanceof Error ? error.message : "Unknown error",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3dpc2hsaXN0cy9hbmFseXRpY3Mvcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFVQSxrQkFtRUM7QUE1RUQscURBQXNFO0FBQ3RFLDJEQUErRDtBQUUvRCw2Q0FBNEQ7QUFNckQsS0FBSyxVQUFVLEdBQUcsQ0FBQyxHQUFrQixFQUFFLEdBQW1CO0lBQy9ELE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLGlDQUF5QixDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQ25FLE1BQU0sS0FBSyxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLGlDQUF5QixDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ2pFLE1BQU0sT0FBTyxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUF3QiwwQkFBZSxDQUFDLENBQUM7SUFFMUUsTUFBTSxNQUFNLEdBQUcseUNBQTRCLENBQUMsU0FBUyxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUNqRSxJQUFJLENBQUMsTUFBTSxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQ3BCLE9BQU8sR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUM7WUFDMUIsS0FBSyxFQUFFLHlCQUF5QjtZQUNoQyxNQUFNLEVBQUUsTUFBTSxDQUFDLEtBQUssQ0FBQyxPQUFPLEVBQUU7U0FDL0IsQ0FBQyxDQUFDO0lBQ0wsQ0FBQztJQUVELElBQUksQ0FBQztRQUNILE1BQU0sSUFBSSxHQUFHLE1BQU0sT0FBTyxDQUFDLFlBQVksQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUM7UUFFckQsTUFBTSxVQUFVLEdBQUcsSUFBSSxDQUFDLFlBQVksQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxV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
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3dpc2hsaXN0cy9hbmFseXRpY3MvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiJ9
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WishlistAnalyticsQuerySchema = void 0;
|
|
4
|
+
const zod_1 = require("@medusajs/framework/zod");
|
|
5
|
+
exports.WishlistAnalyticsQuerySchema = zod_1.z.object({
|
|
6
|
+
from: zod_1.z.string().datetime().optional(),
|
|
7
|
+
to: zod_1.z.string().datetime().optional(),
|
|
8
|
+
sales_channel_id: zod_1.z.string().optional(),
|
|
9
|
+
});
|
|
10
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdG9ycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uLy4uL3NyYy9hcGkvYWRtaW4vd2lzaGxpc3RzL2FuYWx5dGljcy92YWxpZGF0b3JzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLGlEQUE0QztBQUUvQixRQUFBLDRCQUE0QixHQUFHLE9BQUMsQ0FBQyxNQUFNLENBQUM7SUFDbkQsSUFBSSxFQUFFLE9BQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxRQUFRLEVBQUU7SUFDdEMsRUFBRSxFQUFFLE9BQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxRQUFRLEVBQUU7SUFDcEMsZ0JBQWdCLEVBQUUsT0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLFFBQVEsRUFBRTtDQUN4QyxDQUFDLENBQUMifQ==
|
|
@@ -53,6 +53,173 @@ class WishlistModuleService extends (0, utils_1.MedusaService)({
|
|
|
53
53
|
.where("wi.product_id = ?", [productId])
|
|
54
54
|
.execute())?.length || 0);
|
|
55
55
|
}
|
|
56
|
+
async getAnalytics(params = {}, context = {}) {
|
|
57
|
+
const knex = context.manager.getConnection().getKnex();
|
|
58
|
+
const scopeItemsToChannel = (qb) => {
|
|
59
|
+
if (channelId) {
|
|
60
|
+
qb.join("wishlist as w", "w.id", "wi.wishlist_id").where("w.sales_channel_id", channelId);
|
|
61
|
+
}
|
|
62
|
+
return qb;
|
|
63
|
+
};
|
|
64
|
+
const n = (v) => Number(v ?? 0);
|
|
65
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
66
|
+
const to = params.to ? new Date(params.to) : new Date();
|
|
67
|
+
const from = params.from
|
|
68
|
+
? new Date(params.from)
|
|
69
|
+
: new Date(to.getTime() - 30 * DAY_MS);
|
|
70
|
+
const periodMs = to.getTime() - from.getTime();
|
|
71
|
+
const prevFrom = new Date(from.getTime() - periodMs);
|
|
72
|
+
const prevTo = from;
|
|
73
|
+
const channelId = params.sales_channel_id;
|
|
74
|
+
const bucket = periodMs <= 60 * DAY_MS ? "day" : "week";
|
|
75
|
+
const mkDelta = (value, previous) => ({
|
|
76
|
+
value,
|
|
77
|
+
previous,
|
|
78
|
+
delta_pct: previous === 0
|
|
79
|
+
? value > 0
|
|
80
|
+
? 100
|
|
81
|
+
: 0
|
|
82
|
+
: Math.round(((value - previous) / previous) * 1000) / 10,
|
|
83
|
+
});
|
|
84
|
+
const countWishlists = async (lo, hi) => {
|
|
85
|
+
const qb = knex("wishlist")
|
|
86
|
+
.where("created_at", ">=", lo)
|
|
87
|
+
.andWhere("created_at", "<", hi);
|
|
88
|
+
if (channelId)
|
|
89
|
+
qb.where("sales_channel_id", channelId);
|
|
90
|
+
const row = await qb.count("* as c");
|
|
91
|
+
return n(row[0]?.c);
|
|
92
|
+
};
|
|
93
|
+
const countItems = async (lo, hi) => {
|
|
94
|
+
const qb = scopeItemsToChannel(knex("wishlist_item as wi")
|
|
95
|
+
.where("wi.created_at", ">=", lo)
|
|
96
|
+
.andWhere("wi.created_at", "<", hi));
|
|
97
|
+
const row = await qb.count("* as c");
|
|
98
|
+
return n(row[0]?.c);
|
|
99
|
+
};
|
|
100
|
+
const countUniqueCustomers = async (lo, hi) => {
|
|
101
|
+
const qb = knex("wishlist")
|
|
102
|
+
.where("created_at", ">=", lo)
|
|
103
|
+
.andWhere("created_at", "<", hi)
|
|
104
|
+
.whereNotNull("customer_id");
|
|
105
|
+
if (channelId)
|
|
106
|
+
qb.where("sales_channel_id", channelId);
|
|
107
|
+
const row = await qb.countDistinct("customer_id as c");
|
|
108
|
+
return n(row[0]?.c);
|
|
109
|
+
};
|
|
110
|
+
const [totalWishlists, prevTotalWishlists, totalItems, prevTotalItems, uniqueCustomers, prevUniqueCustomers,] = await Promise.all([
|
|
111
|
+
countWishlists(from, to),
|
|
112
|
+
countWishlists(prevFrom, prevTo),
|
|
113
|
+
countItems(from, to),
|
|
114
|
+
countItems(prevFrom, prevTo),
|
|
115
|
+
countUniqueCustomers(from, to),
|
|
116
|
+
countUniqueCustomers(prevFrom, prevTo),
|
|
117
|
+
]);
|
|
118
|
+
const activeQb = knex("wishlist as w")
|
|
119
|
+
.where("w.created_at", ">=", from)
|
|
120
|
+
.andWhere("w.created_at", "<", to)
|
|
121
|
+
.whereExists(function () {
|
|
122
|
+
this.select(knex.raw("1"))
|
|
123
|
+
.from("wishlist_item as wi")
|
|
124
|
+
.where("wi.wishlist_id", knex.ref("w.id"));
|
|
125
|
+
});
|
|
126
|
+
if (channelId)
|
|
127
|
+
activeQb.where("w.sales_channel_id", channelId);
|
|
128
|
+
const activeWishlists = n((await activeQb.count("* as c"))[0]?.c);
|
|
129
|
+
const emptyWishlists = totalWishlists - activeWishlists;
|
|
130
|
+
const guestQb = knex("wishlist")
|
|
131
|
+
.where("created_at", ">=", from)
|
|
132
|
+
.andWhere("created_at", "<", to)
|
|
133
|
+
.whereNull("customer_id");
|
|
134
|
+
if (channelId)
|
|
135
|
+
guestQb.where("sales_channel_id", channelId);
|
|
136
|
+
const guestWishlists = n((await guestQb.count("* as c"))[0]?.c);
|
|
137
|
+
const registeredWishlists = totalWishlists - guestWishlists;
|
|
138
|
+
const wishlistTrendQb = knex("wishlist")
|
|
139
|
+
.where("created_at", ">=", from)
|
|
140
|
+
.andWhere("created_at", "<", to)
|
|
141
|
+
.select(knex.raw("to_char(date_trunc(?, created_at), 'YYYY-MM-DD') as date", [
|
|
142
|
+
bucket,
|
|
143
|
+
]))
|
|
144
|
+
.count("* as count")
|
|
145
|
+
// Group/order by the SELECT ordinal: repeating date_trunc(?, ...) here
|
|
146
|
+
// would bind a *separate* parameter, which Postgres does not treat as the
|
|
147
|
+
// same expression as the one in SELECT ("must appear in GROUP BY").
|
|
148
|
+
.groupByRaw("1")
|
|
149
|
+
.orderByRaw("1");
|
|
150
|
+
if (channelId)
|
|
151
|
+
wishlistTrendQb.where("sales_channel_id", channelId);
|
|
152
|
+
const itemTrendQb = scopeItemsToChannel(knex("wishlist_item as wi")
|
|
153
|
+
.where("wi.created_at", ">=", from)
|
|
154
|
+
.andWhere("wi.created_at", "<", to)
|
|
155
|
+
.select(knex.raw("to_char(date_trunc(?, wi.created_at), 'YYYY-MM-DD') as date", [bucket]))
|
|
156
|
+
.count("* as count")
|
|
157
|
+
.groupByRaw("1")
|
|
158
|
+
.orderByRaw("1"));
|
|
159
|
+
const [wishlistTrendRows, itemTrendRows] = (await Promise.all([
|
|
160
|
+
wishlistTrendQb,
|
|
161
|
+
itemTrendQb,
|
|
162
|
+
]));
|
|
163
|
+
const trendMap = new Map();
|
|
164
|
+
for (const r of wishlistTrendRows) {
|
|
165
|
+
trendMap.set(r.date, { wishlists: n(r.count), items: 0 });
|
|
166
|
+
}
|
|
167
|
+
for (const r of itemTrendRows) {
|
|
168
|
+
const e = trendMap.get(r.date) ?? { wishlists: 0, items: 0 };
|
|
169
|
+
e.items = n(r.count);
|
|
170
|
+
trendMap.set(r.date, e);
|
|
171
|
+
}
|
|
172
|
+
const trend = [...trendMap.entries()]
|
|
173
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
174
|
+
.map(([date, v]) => ({ date, wishlists: v.wishlists, items: v.items }));
|
|
175
|
+
const topProductsQb = scopeItemsToChannel(knex("wishlist_item as wi")
|
|
176
|
+
.where("wi.created_at", ">=", from)
|
|
177
|
+
.andWhere("wi.created_at", "<", to));
|
|
178
|
+
const topProductsRows = (await topProductsQb
|
|
179
|
+
.select("wi.product_id")
|
|
180
|
+
.countDistinct("wi.wishlist_id as wishlist_count")
|
|
181
|
+
.count("* as item_count")
|
|
182
|
+
.groupBy("wi.product_id")
|
|
183
|
+
.orderBy("wishlist_count", "desc")
|
|
184
|
+
.limit(10));
|
|
185
|
+
const topVariantsQb = scopeItemsToChannel(knex("wishlist_item as wi")
|
|
186
|
+
.where("wi.created_at", ">=", from)
|
|
187
|
+
.andWhere("wi.created_at", "<", to));
|
|
188
|
+
const topVariantsRows = (await topVariantsQb
|
|
189
|
+
.select("wi.product_variant_id", "wi.product_id")
|
|
190
|
+
.countDistinct("wi.wishlist_id as wishlist_count")
|
|
191
|
+
.groupBy("wi.product_variant_id", "wi.product_id")
|
|
192
|
+
.orderBy("wishlist_count", "desc")
|
|
193
|
+
.limit(10));
|
|
194
|
+
const avgCurrent = totalWishlists ? totalItems / totalWishlists : 0;
|
|
195
|
+
const avgPrev = prevTotalWishlists
|
|
196
|
+
? prevTotalItems / prevTotalWishlists
|
|
197
|
+
: 0;
|
|
198
|
+
return {
|
|
199
|
+
range: { from: from.toISOString(), to: to.toISOString() },
|
|
200
|
+
kpis: {
|
|
201
|
+
total_wishlists: mkDelta(totalWishlists, prevTotalWishlists),
|
|
202
|
+
total_items: mkDelta(totalItems, prevTotalItems),
|
|
203
|
+
avg_items_per_wishlist: mkDelta(Math.round(avgCurrent * 100) / 100, Math.round(avgPrev * 100) / 100),
|
|
204
|
+
active_wishlists: activeWishlists,
|
|
205
|
+
empty_wishlists: emptyWishlists,
|
|
206
|
+
guest_wishlists: guestWishlists,
|
|
207
|
+
registered_wishlists: registeredWishlists,
|
|
208
|
+
unique_customers: mkDelta(uniqueCustomers, prevUniqueCustomers),
|
|
209
|
+
},
|
|
210
|
+
trend,
|
|
211
|
+
top_products: topProductsRows.map((r) => ({
|
|
212
|
+
product_id: r.product_id,
|
|
213
|
+
wishlist_count: n(r.wishlist_count),
|
|
214
|
+
item_count: n(r.item_count),
|
|
215
|
+
})),
|
|
216
|
+
top_variants: topVariantsRows.map((r) => ({
|
|
217
|
+
product_variant_id: r.product_variant_id,
|
|
218
|
+
product_id: r.product_id,
|
|
219
|
+
wishlist_count: n(r.wishlist_count),
|
|
220
|
+
})),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
56
223
|
async totalItemsCount({ customer_id, wishlist_id, }, context = {}) {
|
|
57
224
|
const wishlist_items_count = await context.manager?.count(wishlist_item_1.WishlistItem, {
|
|
58
225
|
wishlist: {
|
|
@@ -133,6 +300,13 @@ __decorate([
|
|
|
133
300
|
__metadata("design:paramtypes", [String, Object]),
|
|
134
301
|
__metadata("design:returntype", Promise)
|
|
135
302
|
], WishlistModuleService.prototype, "getWishlistCountsOfProduct", null);
|
|
303
|
+
__decorate([
|
|
304
|
+
(0, utils_2.InjectManager)(),
|
|
305
|
+
__param(1, (0, utils_1.MedusaContext)()),
|
|
306
|
+
__metadata("design:type", Function),
|
|
307
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
308
|
+
__metadata("design:returntype", Promise)
|
|
309
|
+
], WishlistModuleService.prototype, "getAnalytics", null);
|
|
136
310
|
__decorate([
|
|
137
311
|
(0, utils_2.InjectManager)(),
|
|
138
312
|
__param(1, (0, utils_1.MedusaContext)()),
|
|
@@ -140,4 +314,4 @@ __decorate([
|
|
|
140
314
|
__metadata("design:paramtypes", [Object, Object]),
|
|
141
315
|
__metadata("design:returntype", Promise)
|
|
142
316
|
], WishlistModuleService.prototype, "totalItemsCount", null);
|
|
143
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9tb2R1bGVzL3dpc2hsaXN0L3NlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQSxxREFJbUM7QUFDbkMsZ0RBQTZDO0FBQzdDLDBEQUFzRDtBQUN0RCxrRUFBOEQ7QUFDOUQscURBQTBEO0FBRzFELGdFQUErQjtBQUMvQixpREFBNEM7QUFPNUMsTUFBTSw4QkFBOEIsR0FBRyxlQUFlLENBQUM7QUEyQ3ZELE1BQU0sYUFBYSxHQUFHLE9BQUMsQ0FBQyxNQUFNLENBQUM7SUFDN0IsY0FBYyxFQUFFLE9BQUMsQ0FBQyxLQUFLLENBQUMsT0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUMsUUFBUSxFQUFFO0lBQzlDLG1CQUFtQixFQUFFLE9BQUMsQ0FBQyxLQUFLLENBQUMsT0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUMsUUFBUSxFQUFFO0lBQ25ELG9CQUFvQixFQUFFLE9BQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDO0lBQ2hELHdCQUF3QixFQUFFLE9BQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQy9DLGtCQUFrQixFQUFFLE9BQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDO0lBQzlDLGdCQUFnQixFQUFFLE9BQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxPQUFPLENBQUMsZ0JBQWdCLENBQUM7Q0FDdkQsQ0FBQyxDQUFDO0FBSUgsTUFBcUIscUJBQXNCLFNBQVEsSUFBQSxxQkFBYSxFQUFDO0lBQy9ELFFBQVEsRUFBUixtQkFBUTtJQUNSLFlBQVksRUFBWiw0QkFBWTtJQUNaLGdCQUFnQixFQUFoQixvQ0FBZ0I7Q0FDakIsQ0FBQztJQUdBLE1BQU0sQ0FBQyxlQUFlLENBQ3BCLFFBQTRDO1FBRTVDLE1BQU0s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
|
|
317
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9tb2R1bGVzL3dpc2hsaXN0L3NlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQSxxREFJbUM7QUFDbkMsZ0RBQTZDO0FBQzdDLDBEQUFzRDtBQUN0RCxrRUFBOEQ7QUFDOUQscURBQTBEO0FBSTFELGdFQUErQjtBQUMvQixpREFBNEM7QUFZNUMsTUFBTSw4QkFBOEIsR0FBRyxlQUFlLENBQUM7QUEyQ3ZELE1BQU0sYUFBYSxHQUFHLE9BQUMsQ0FBQyxNQUFNLENBQUM7SUFDN0IsY0FBYyxFQUFFLE9BQUMsQ0FBQyxLQUFLLENBQUMsT0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUMsUUFBUSxFQUFFO0lBQzlDLG1CQUFtQixFQUFFLE9BQUMsQ0FBQyxLQUFLLENBQUMsT0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUMsUUFBUSxFQUFFO0lBQ25ELG9CQUFvQixFQUFFLE9BQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDO0lBQ2hELHdCQUF3QixFQUFFLE9BQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQy9DLGtCQUFrQixFQUFFLE9BQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDO0lBQzlDLGdCQUFnQixFQUFFLE9BQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxPQUFPLENBQUMsZ0JBQWdCLENBQUM7Q0FDdkQsQ0FBQyxDQUFDO0FBSUgsTUFBcUIscUJBQXNCLFNBQVEsSUFBQSxxQkFBYSxFQUFDO0lBQy9ELFFBQVEsRUFBUixtQkFBUTtJQUNSLFlBQVksRUFBWiw0QkFBWTtJQUNaLGdCQUFnQixFQUFoQixvQ0FBZ0I7Q0FDakIsQ0FBQztJQUdBLE1BQU0sQ0FBQyxlQUFlLENBQ3BCLFFBQTRDO1FBRTVDLE1BQU0s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
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYW5hbHl0aWNzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vLi4vLi4vc3JjL21vZHVsZXMvd2lzaGxpc3QvdHlwZXMvYW5hbHl0aWNzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiIifQ==
|
|
@@ -3,6 +3,7 @@ import { EntityManager } from "@mikro-orm/knex";
|
|
|
3
3
|
import { z } from "@medusajs/framework/zod";
|
|
4
4
|
import { Wishlist as WishlistType } from "../../api/store/wishlists/types";
|
|
5
5
|
import type { WishlistSettingsView, WishlistSettingsPatch } from "./types/settings";
|
|
6
|
+
import type { WishlistAnalyticsParams, WishlistAnalyticsView } from "./types/analytics";
|
|
6
7
|
/**
|
|
7
8
|
* Options for configuring the Alphabite Wishlist Plugin
|
|
8
9
|
*/
|
|
@@ -83,6 +84,7 @@ export default class WishlistModuleService extends WishlistModuleService_base {
|
|
|
83
84
|
static validateOptions(_options: AlphabiteWishlistPluginOptionsType): void | never;
|
|
84
85
|
constructor({}: {}, options: AlphabiteWishlistPluginOptionsType);
|
|
85
86
|
getWishlistCountsOfProduct(productId: string, context?: Context<EntityManager>): Promise<number>;
|
|
87
|
+
getAnalytics(params?: WishlistAnalyticsParams, context?: Context<EntityManager>): Promise<WishlistAnalyticsView>;
|
|
86
88
|
totalItemsCount({ customer_id, wishlist_id, }: {
|
|
87
89
|
customer_id?: string;
|
|
88
90
|
wishlist_id?: string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type WishlistAnalyticsParams = {
|
|
2
|
+
/** ISO date string; defaults to 30 days before `to`. */
|
|
3
|
+
from?: string;
|
|
4
|
+
/** ISO date string; defaults to now. */
|
|
5
|
+
to?: string;
|
|
6
|
+
/** Restrict all metrics to a single sales channel. */
|
|
7
|
+
sales_channel_id?: string;
|
|
8
|
+
};
|
|
9
|
+
export type Delta = {
|
|
10
|
+
value: number;
|
|
11
|
+
previous: number;
|
|
12
|
+
/** Percent change vs the previous equal-length period, rounded to 1 decimal. */
|
|
13
|
+
delta_pct: number;
|
|
14
|
+
};
|
|
15
|
+
export type WishlistAnalyticsView = {
|
|
16
|
+
range: {
|
|
17
|
+
from: string;
|
|
18
|
+
to: string;
|
|
19
|
+
};
|
|
20
|
+
kpis: {
|
|
21
|
+
total_wishlists: Delta;
|
|
22
|
+
total_items: Delta;
|
|
23
|
+
avg_items_per_wishlist: Delta;
|
|
24
|
+
active_wishlists: number;
|
|
25
|
+
empty_wishlists: number;
|
|
26
|
+
guest_wishlists: number;
|
|
27
|
+
registered_wishlists: number;
|
|
28
|
+
unique_customers: Delta;
|
|
29
|
+
};
|
|
30
|
+
trend: Array<{
|
|
31
|
+
date: string;
|
|
32
|
+
wishlists: number;
|
|
33
|
+
items: number;
|
|
34
|
+
}>;
|
|
35
|
+
top_products: Array<{
|
|
36
|
+
product_id: string;
|
|
37
|
+
wishlist_count: number;
|
|
38
|
+
item_count: number;
|
|
39
|
+
}>;
|
|
40
|
+
top_variants: Array<{
|
|
41
|
+
product_variant_id: string;
|
|
42
|
+
product_id: string;
|
|
43
|
+
wishlist_count: number;
|
|
44
|
+
}>;
|
|
45
|
+
};
|
package/README.md
CHANGED
|
@@ -47,6 +47,7 @@ npm install @alphabite/medusa-wishlist@0.5.7
|
|
|
47
47
|
- [✨ Features](#-features)
|
|
48
48
|
- [📦 Installation](#-installation)
|
|
49
49
|
- [🔧 Plugin Options](#-plugin-options)
|
|
50
|
+
- [🛠️ Admin](#-admin)
|
|
50
51
|
- [📦 API Endpoints](#-api-endpoints)
|
|
51
52
|
- [🧑💻 SDK Usage](#-sdk-usage)
|
|
52
53
|
- [🧪 Guest Wishlist Flow](#-guest-wishlist-flow)
|
|
@@ -63,6 +64,7 @@ npm install @alphabite/medusa-wishlist@0.5.7
|
|
|
63
64
|
- ✅ Guest wishlist supported + transfer when registered
|
|
64
65
|
- ✅ Fully typed Medusa JS SDK integration with our SDK client
|
|
65
66
|
- ✅ Pagination and filtering built-in
|
|
67
|
+
- ✅ Admin page with **Settings** + **Analytics** tabs
|
|
66
68
|
|
|
67
69
|
---
|
|
68
70
|
|
|
@@ -106,6 +108,18 @@ const plugins = [
|
|
|
106
108
|
|
|
107
109
|
---
|
|
108
110
|
|
|
111
|
+
## 🛠️ Admin
|
|
112
|
+
|
|
113
|
+
The plugin adds a **Wishlists** page to the admin sidebar (`/app/wishlists`) with
|
|
114
|
+
two tabs: **Settings** (guest / multiple-wishlist toggles) and **Analytics** (a
|
|
115
|
+
read-only dashboard of KPIs, an activity trend, and the most-wishlisted products
|
|
116
|
+
and variants).
|
|
117
|
+
|
|
118
|
+
See [docs/admin-analytics.md](docs/admin-analytics.md) for the analytics endpoint
|
|
119
|
+
and response shape.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
109
123
|
## 📦 API Endpoints
|
|
110
124
|
|
|
111
125
|
All endpoints are available under `/store/wishlists`.
|