@kb0912/notification-brevo 1.0.17 → 2.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.
Files changed (40) hide show
  1. package/.medusa/server/src/admin/index.js +1004 -3
  2. package/.medusa/server/src/admin/index.mjs +1004 -3
  3. package/.medusa/server/src/api/admin/brevo-plugin-settings/analytics/route.js +78 -0
  4. package/.medusa/server/src/api/admin/brevo-plugin-settings/route.js +27 -0
  5. package/.medusa/server/src/api/hooks/brevo-webhook/route.js +40 -0
  6. package/.medusa/server/src/jobs/abandoned-cart.js +5 -11
  7. package/.medusa/server/src/jobs/promotion-expiry-reminder.js +122 -0
  8. package/.medusa/server/src/jobs/review-request.js +106 -0
  9. package/.medusa/server/src/jobs/winback.js +108 -0
  10. package/.medusa/server/src/modules/brevo-settings/index.js +13 -0
  11. package/.medusa/server/src/modules/brevo-settings/migrations/Migration20260306173214.js +15 -0
  12. package/.medusa/server/src/modules/brevo-settings/migrations/Migration20260307045432.js +14 -0
  13. package/.medusa/server/src/modules/brevo-settings/migrations/Migration20260307075811.js +14 -0
  14. package/.medusa/server/src/modules/brevo-settings/models/brevo-settings.js +79 -0
  15. package/.medusa/server/src/modules/brevo-settings/service.js +114 -0
  16. package/.medusa/server/src/providers/notifications-brevo/services.js +325 -176
  17. package/.medusa/server/src/workflows/index.js +25 -0
  18. package/.medusa/server/src/workflows/send-abandoned-cart.js +37 -8
  19. package/.medusa/server/src/workflows/send-customer-created.js +159 -4
  20. package/.medusa/server/src/workflows/send-order-canceled.js +27 -3
  21. package/.medusa/server/src/workflows/send-order-confirmation.js +63 -10
  22. package/.medusa/server/src/workflows/send-order-delivered.js +52 -0
  23. package/.medusa/server/src/workflows/send-review-request.js +37 -0
  24. package/.medusa/server/src/workflows/send-shipment-confirmation.js +33 -10
  25. package/.medusa/server/src/workflows/send-winback.js +34 -0
  26. package/.medusa/server/src/workflows/steps/check-abandoned-carts.js +184 -58
  27. package/.medusa/server/src/workflows/steps/resolve-locale.js +25 -0
  28. package/.medusa/server/src/workflows/steps/send-multi-channel.js +81 -0
  29. package/.medusa/server/src/workflows/steps/send-notification.js +1 -1
  30. package/.medusa/server/src/workflows/steps/sync-brevo-contact.js +57 -0
  31. package/.medusa/server/src/workflows/steps/track-brevo-event.js +47 -0
  32. package/README.md +315 -60
  33. package/package.json +20 -23
  34. package/.medusa/server/src/api/admin/plugin/route.js +0 -7
  35. package/.medusa/server/src/config.js +0 -12
  36. package/.medusa/server/src/loaders/brevo.js +0 -13
  37. package/.medusa/server/src/subscribers/password-reset.js +0 -18
  38. package/.medusa/server/src/subscribers/promotion-new-customer-created.js +0 -21
  39. package/.medusa/server/src/workflows/send-password-reset.js +0 -30
  40. package/.medusa/server/src/workflows/send-promotion-notification.js +0 -24
@@ -1,20 +1,1021 @@
1
1
  "use strict";
2
+ const jsxRuntime = require("react/jsx-runtime");
3
+ const adminSdk = require("@medusajs/admin-sdk");
4
+ const ui = require("@medusajs/ui");
5
+ const icons = require("@medusajs/icons");
6
+ const react = require("react");
7
+ const EVENT_LABELS = {
8
+ "order.placed": "Order Placed",
9
+ "order.canceled": "Order Canceled",
10
+ "order.delivered": "Order Delivered",
11
+ "customer.created": "Customer Created",
12
+ "promotion-new-customer": "Promotion (New Customer)",
13
+ "promotion-expiry-reminder": "Promotion Expiry Reminder",
14
+ "shipment.confirmed": "Shipment Confirmed",
15
+ "cart.abandoned": "Abandoned Cart",
16
+ "cart.abandoned.discount": "Abandoned Cart (Discount)",
17
+ "review.request": "Review Request",
18
+ "winback": "Win-back"
19
+ };
20
+ const BrevoAnalyticsPage = () => {
21
+ const [data, setData] = react.useState(null);
22
+ const [loading, setLoading] = react.useState(true);
23
+ const [error, setError] = react.useState(null);
24
+ react.useEffect(() => {
25
+ fetch("/admin/brevo-plugin-settings/analytics", { credentials: "include" }).then((res) => res.json()).then((d) => {
26
+ setData(d);
27
+ setLoading(false);
28
+ }).catch(() => {
29
+ setError("Failed to load analytics. Make sure Brevo API key is configured.");
30
+ setLoading(false);
31
+ });
32
+ }, []);
33
+ if (loading) {
34
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading analytics..." }) }) });
35
+ }
36
+ if (error || !(data == null ? void 0 : data.stats)) {
37
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", children: error || (data == null ? void 0 : data.error) || "No data available" }) }) });
38
+ }
39
+ const { stats, perEvent } = data;
40
+ const statItems = [
41
+ { label: "Delivered", value: stats.delivered, color: "green" },
42
+ { label: "Opened", value: stats.opened, color: "blue" },
43
+ { label: "Clicked", value: stats.clicked, color: "purple" },
44
+ { label: "Bounced", value: stats.bounced, color: "orange" },
45
+ { label: "Blocked", value: stats.blocked, color: "red" },
46
+ { label: "Unsubscribed", value: stats.unsubscribed, color: "grey" }
47
+ ];
48
+ const openRate = stats.delivered > 0 ? (stats.opened / stats.delivered * 100).toFixed(1) : "0";
49
+ const clickRate = stats.opened > 0 ? (stats.clicked / stats.opened * 100).toFixed(1) : "0";
50
+ const perEventEntries = Object.entries(perEvent || {}).sort((a, b) => b[1].delivered - a[1].delivered);
51
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
52
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
53
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Email Analytics" }),
54
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mt-1", children: "Transactional email performance from Brevo (last 30 days)" })
55
+ ] }) }),
56
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4 mt-4", children: statItems.map(({ label, value, color }) => /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "p-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 text-center", children: [
57
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs uppercase", children: label }),
58
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mt-1", children: value.toLocaleString() }),
59
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color, className: "mt-2", children: label })
60
+ ] }) }, label)) }),
61
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
62
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Conversion Rates" }),
63
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
64
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
65
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Metric" }),
66
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Rate" }),
67
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Details" })
68
+ ] }) }),
69
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Body, { children: [
70
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
71
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: "Open Rate" }) }),
72
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { color: "blue", children: [
73
+ openRate,
74
+ "%"
75
+ ] }) }),
76
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { className: "text-ui-fg-subtle", children: [
77
+ stats.opened,
78
+ " / ",
79
+ stats.delivered,
80
+ " delivered"
81
+ ] }) })
82
+ ] }),
83
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
84
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: "Click Rate" }) }),
85
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { color: "purple", children: [
86
+ clickRate,
87
+ "%"
88
+ ] }) }),
89
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { className: "text-ui-fg-subtle", children: [
90
+ stats.clicked,
91
+ " / ",
92
+ stats.opened,
93
+ " opened"
94
+ ] }) })
95
+ ] }),
96
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
97
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: "Bounce Rate" }) }),
98
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { color: "orange", children: [
99
+ stats.delivered > 0 ? (stats.bounced / (stats.delivered + stats.bounced) * 100).toFixed(1) : "0",
100
+ "%"
101
+ ] }) }),
102
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { className: "text-ui-fg-subtle", children: [
103
+ stats.bounced,
104
+ " bounced"
105
+ ] }) })
106
+ ] })
107
+ ] })
108
+ ] })
109
+ ] }) }),
110
+ perEventEntries.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
111
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-2", children: "Per-Event Breakdown" }),
112
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mb-4", children: "Performance by email event type (only events with activity shown)" }),
113
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
114
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
115
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Event" }),
116
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Sent" }),
117
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Opened" }),
118
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Clicked" }),
119
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Open Rate" }),
120
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Bounced" })
121
+ ] }) }),
122
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: perEventEntries.map(([event, eventStats]) => {
123
+ const evOpenRate = eventStats.delivered > 0 ? (eventStats.opened / eventStats.delivered * 100).toFixed(1) : "0";
124
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
125
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: EVENT_LABELS[event] || event }) }),
126
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: eventStats.delivered.toLocaleString() }),
127
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: eventStats.opened.toLocaleString() }),
128
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: eventStats.clicked.toLocaleString() }),
129
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { color: "blue", children: [
130
+ evOpenRate,
131
+ "%"
132
+ ] }) }),
133
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: eventStats.bounced > 0 ? eventStats.bounced.toLocaleString() : "—" })
134
+ ] }, event);
135
+ }) })
136
+ ] })
137
+ ] }) }),
138
+ perEventEntries.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
139
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-2", children: "Per-Event Breakdown" }),
140
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "No per-event data yet. Emails sent after this update will be tagged automatically and stats will appear here." })
141
+ ] }) })
142
+ ] });
143
+ };
144
+ const config$1 = adminSdk.defineRouteConfig({
145
+ label: "Email Analytics",
146
+ icon: icons.ChartBar
147
+ });
148
+ const BrevoSettingsPage = () => {
149
+ const [settings, setSettings] = react.useState(null);
150
+ const [loading, setLoading] = react.useState(true);
151
+ const [saving, setSaving] = react.useState(false);
152
+ const [intervalsText, setIntervalsText] = react.useState("");
153
+ const [currencies, setCurrencies] = react.useState([]);
154
+ react.useEffect(() => {
155
+ Promise.all([
156
+ fetch("/admin/brevo-plugin-settings", { credentials: "include" }).then((r) => r.json()),
157
+ fetch("/admin/currencies", { credentials: "include" }).then((r) => r.json()).catch(() => ({ currencies: [] }))
158
+ ]).then(([settingsData, currData]) => {
159
+ setSettings(settingsData.settings);
160
+ const intervals = Array.isArray(settingsData.settings.abandoned_cart_intervals) ? settingsData.settings.abandoned_cart_intervals : [];
161
+ setIntervalsText(intervals.join(", "));
162
+ const codes = (currData.currencies || []).map((c) => c.code).filter(Boolean).sort();
163
+ setCurrencies(codes);
164
+ setLoading(false);
165
+ }).catch(() => {
166
+ ui.toast.error("Failed to load Brevo settings");
167
+ setLoading(false);
168
+ });
169
+ }, []);
170
+ const handleSave = async () => {
171
+ if (!settings) return;
172
+ setSaving(true);
173
+ const intervals = intervalsText.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n) && n > 0);
174
+ try {
175
+ const res = await fetch("/admin/brevo-plugin-settings", {
176
+ method: "POST",
177
+ credentials: "include",
178
+ headers: { "Content-Type": "application/json" },
179
+ body: JSON.stringify({
180
+ ...settings,
181
+ abandoned_cart_intervals: intervals
182
+ })
183
+ });
184
+ if (!res.ok) throw new Error("Save failed");
185
+ const getRes = await fetch("/admin/brevo-plugin-settings", { credentials: "include" });
186
+ const data = await getRes.json();
187
+ setSettings(data.settings);
188
+ const newIntervals = Array.isArray(data.settings.abandoned_cart_intervals) ? data.settings.abandoned_cart_intervals : [];
189
+ setIntervalsText(newIntervals.join(", "));
190
+ ui.toast.success("Settings saved successfully");
191
+ } catch {
192
+ ui.toast.error("Failed to save settings");
193
+ } finally {
194
+ setSaving(false);
195
+ }
196
+ };
197
+ const update = (key, value) => {
198
+ setSettings((prev) => prev ? { ...prev, [key]: value } : prev);
199
+ };
200
+ if (loading) {
201
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading Brevo settings..." }) }) });
202
+ }
203
+ if (!settings) {
204
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", children: "Failed to load settings." }) }) });
205
+ }
206
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
207
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Toaster, {}),
208
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
209
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
210
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Brevo Email Settings" }),
211
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mt-1", children: "Configure all email, SMS, WhatsApp, and automation settings." })
212
+ ] }),
213
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: handleSave, isLoading: saving, children: "Save Changes" })
214
+ ] }) }),
215
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
216
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Sender" }),
217
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-4 max-w-md", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
218
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "sender_name", children: "Sender Name" }),
219
+ /* @__PURE__ */ jsxRuntime.jsx(
220
+ ui.Input,
221
+ {
222
+ id: "sender_name",
223
+ placeholder: "My Store",
224
+ value: settings.sender_name || "",
225
+ onChange: (e) => update("sender_name", e.target.value)
226
+ }
227
+ ),
228
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs mt-1", children: 'Display name in "From" field. Email is set via BREVO_FROM_EMAIL.' })
229
+ ] }) })
230
+ ] }) }),
231
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
232
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Email Templates" }),
233
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mb-4", children: "Brevo template ID for each event. Toggle to enable/disable." }),
234
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-4", children: [
235
+ { key: "order_placed", label: "Order Placed" },
236
+ { key: "order_canceled", label: "Order Canceled" },
237
+ { key: "order_delivered", label: "Order Delivered" },
238
+ { key: "customer_created", label: "Customer Created" },
239
+ { key: "shipment_confirmed", label: "Shipment Confirmed" }
240
+ ].map(({ key, label, ...rest }) => {
241
+ const enabledField = "enabledKey" in rest ? rest.enabledKey : `${key}_enabled`;
242
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 p-3 border rounded-lg", children: [
243
+ /* @__PURE__ */ jsxRuntime.jsx(
244
+ ui.Switch,
245
+ {
246
+ checked: settings[enabledField],
247
+ onCheckedChange: (v) => update(enabledField, v)
248
+ }
249
+ ),
250
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: label }) }),
251
+ /* @__PURE__ */ jsxRuntime.jsx(
252
+ ui.Input,
253
+ {
254
+ className: "w-32",
255
+ placeholder: "Template ID",
256
+ value: settings[`${key}_template_id`] || "",
257
+ onChange: (e) => update(`${key}_template_id`, e.target.value)
258
+ }
259
+ )
260
+ ] }, key);
261
+ }) })
262
+ ] }) }),
263
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
264
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Promotion — New Customer" }),
265
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mb-4", children: "Auto-create a discount code when a new customer registers and send it via email." }),
266
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 mb-4 p-3 border rounded-lg", children: [
267
+ /* @__PURE__ */ jsxRuntime.jsx(
268
+ ui.Switch,
269
+ {
270
+ checked: settings.promotion_auto_create,
271
+ onCheckedChange: (v) => {
272
+ update("promotion_auto_create", v);
273
+ update("promotion_enabled", v);
274
+ }
275
+ }
276
+ ),
277
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Auto-create Discount for New Customers" }) })
278
+ ] }),
279
+ settings.promotion_auto_create && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4 max-w-lg ml-4 mt-2", children: [
280
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "col-span-2", children: [
281
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Promotion Email Template ID" }),
282
+ /* @__PURE__ */ jsxRuntime.jsx(
283
+ ui.Input,
284
+ {
285
+ placeholder: "Brevo template ID",
286
+ value: settings.promotion_new_customer_template_id || "",
287
+ onChange: (e) => update("promotion_new_customer_template_id", e.target.value)
288
+ }
289
+ )
290
+ ] }),
291
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
292
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Code Prefix" }),
293
+ /* @__PURE__ */ jsxRuntime.jsx(
294
+ ui.Input,
295
+ {
296
+ placeholder: "WELCOME",
297
+ value: settings.promotion_code_prefix || "WELCOME",
298
+ onChange: (e) => update("promotion_code_prefix", e.target.value.toUpperCase())
299
+ }
300
+ ),
301
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs mt-1", children: "Code format: PREFIX-8CHARS (e.g. WELCOME-A3KF8X2N)" })
302
+ ] }),
303
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
304
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Discount Type" }),
305
+ /* @__PURE__ */ jsxRuntime.jsxs(
306
+ ui.Select,
307
+ {
308
+ value: settings.promotion_discount_type || "percentage",
309
+ onValueChange: (v) => {
310
+ if (v !== (settings.promotion_discount_type || "percentage")) update("promotion_discount_type", v);
311
+ },
312
+ children: [
313
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "Select type" }) }),
314
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
315
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "percentage", children: "Percentage (%)" }),
316
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "fixed", children: "Fixed Amount" })
317
+ ] })
318
+ ]
319
+ }
320
+ )
321
+ ] }),
322
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
323
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Discount Value" }),
324
+ /* @__PURE__ */ jsxRuntime.jsx(
325
+ ui.Input,
326
+ {
327
+ type: "number",
328
+ min: 1,
329
+ value: settings.promotion_discount_value || 10,
330
+ onChange: (e) => update("promotion_discount_value", parseInt(e.target.value, 10) || 10)
331
+ }
332
+ )
333
+ ] }),
334
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
335
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Expires After (days)" }),
336
+ /* @__PURE__ */ jsxRuntime.jsx(
337
+ ui.Input,
338
+ {
339
+ type: "number",
340
+ min: 1,
341
+ max: 365,
342
+ value: settings.promotion_expiry_days || 30,
343
+ onChange: (e) => update("promotion_expiry_days", parseInt(e.target.value, 10) || 30)
344
+ }
345
+ ),
346
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs mt-1", children: "Discount code valid for X days after creation." })
347
+ ] }),
348
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "col-span-2", children: [
349
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Excluded Currencies" }),
350
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs mb-2", children: "Discount will NOT apply to orders in these currencies. Click to toggle." }),
351
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-1", children: [
352
+ currencies.map((cur) => {
353
+ const excluded = Array.isArray(settings.promotion_excluded_currencies) ? settings.promotion_excluded_currencies : [];
354
+ const isExcluded = excluded.includes(cur);
355
+ return /* @__PURE__ */ jsxRuntime.jsxs(
356
+ "button",
357
+ {
358
+ type: "button",
359
+ className: `px-2 py-1 rounded text-xs font-mono border transition-colors ${isExcluded ? "bg-red-100 border-red-300 text-red-700" : "bg-ui-bg-base border-ui-border-base text-ui-fg-subtle hover:bg-ui-bg-base-hover"}`,
360
+ onClick: () => {
361
+ const next = isExcluded ? excluded.filter((c) => c !== cur) : [...excluded, cur];
362
+ update("promotion_excluded_currencies", next);
363
+ },
364
+ children: [
365
+ cur.toUpperCase(),
366
+ isExcluded ? " ✕" : ""
367
+ ]
368
+ },
369
+ cur
370
+ );
371
+ }),
372
+ currencies.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs", children: "No currencies found in store." })
373
+ ] })
374
+ ] })
375
+ ] })
376
+ ] }) }),
377
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
378
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Discount Expiry Reminder" }),
379
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mb-4", children: "Send a reminder email when a customer's discount code is about to expire." }),
380
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 mb-4 p-3 border rounded-lg", children: [
381
+ /* @__PURE__ */ jsxRuntime.jsx(
382
+ ui.Switch,
383
+ {
384
+ checked: settings.promotion_expiry_reminder_enabled,
385
+ onCheckedChange: (v) => update("promotion_expiry_reminder_enabled", v)
386
+ }
387
+ ),
388
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Enable Expiry Reminder" }) }),
389
+ /* @__PURE__ */ jsxRuntime.jsx(
390
+ ui.Input,
391
+ {
392
+ className: "w-32",
393
+ placeholder: "Template ID",
394
+ value: settings.promotion_expiry_reminder_template_id || "",
395
+ onChange: (e) => update("promotion_expiry_reminder_template_id", e.target.value)
396
+ }
397
+ )
398
+ ] }),
399
+ settings.promotion_expiry_reminder_enabled && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-md ml-4", children: [
400
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Days Before Expiry" }),
401
+ /* @__PURE__ */ jsxRuntime.jsx(
402
+ ui.Input,
403
+ {
404
+ type: "number",
405
+ min: 1,
406
+ max: 30,
407
+ value: settings.promotion_expiry_reminder_days_before || 3,
408
+ onChange: (e) => update("promotion_expiry_reminder_days_before", parseInt(e.target.value, 10) || 3)
409
+ }
410
+ ),
411
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs mt-1", children: "Send reminder X days before discount expires." })
412
+ ] })
413
+ ] }) }),
414
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
415
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Abandoned Cart" }),
416
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 mb-4 p-3 border rounded-lg", children: [
417
+ /* @__PURE__ */ jsxRuntime.jsx(
418
+ ui.Switch,
419
+ {
420
+ checked: settings.abandoned_cart_enabled,
421
+ onCheckedChange: (v) => update("abandoned_cart_enabled", v)
422
+ }
423
+ ),
424
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Enable Abandoned Cart Emails" }) }),
425
+ /* @__PURE__ */ jsxRuntime.jsx(
426
+ ui.Input,
427
+ {
428
+ className: "w-32",
429
+ placeholder: "Template ID",
430
+ value: settings.abandoned_cart_template_id || "",
431
+ onChange: (e) => update("abandoned_cart_template_id", e.target.value)
432
+ }
433
+ )
434
+ ] }),
435
+ settings.abandoned_cart_enabled && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-4 max-w-md ml-4 mt-2", children: [
436
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
437
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Reminder Intervals (hours)" }),
438
+ /* @__PURE__ */ jsxRuntime.jsx(
439
+ ui.Input,
440
+ {
441
+ placeholder: "1, 24, 72",
442
+ value: intervalsText,
443
+ onChange: (e) => setIntervalsText(e.target.value)
444
+ }
445
+ ),
446
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs mt-1", children: 'Comma-separated. E.g. "1, 24, 72"' })
447
+ ] }),
448
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
449
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Max Emails Per Cart" }),
450
+ /* @__PURE__ */ jsxRuntime.jsx(
451
+ ui.Input,
452
+ {
453
+ type: "number",
454
+ min: 1,
455
+ max: 10,
456
+ value: settings.abandoned_cart_max_emails,
457
+ onChange: (e) => update("abandoned_cart_max_emails", parseInt(e.target.value, 10) || 1)
458
+ }
459
+ )
460
+ ] })
461
+ ] }),
462
+ settings.abandoned_cart_enabled && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-6 pt-4 border-t", children: [
463
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mb-4 text-sm", children: "Auto-generate a discount code on the final reminder email." }),
464
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 mb-4 p-3 border rounded-lg", children: [
465
+ /* @__PURE__ */ jsxRuntime.jsx(
466
+ ui.Switch,
467
+ {
468
+ checked: settings.abandoned_cart_discount_enabled,
469
+ onCheckedChange: (v) => update("abandoned_cart_discount_enabled", v)
470
+ }
471
+ ),
472
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Enable Discount on Final Email" }) })
473
+ ] }),
474
+ settings.abandoned_cart_discount_enabled && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4 max-w-lg ml-4 mt-2", children: [
475
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
476
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Discount Type" }),
477
+ /* @__PURE__ */ jsxRuntime.jsxs(
478
+ ui.Select,
479
+ {
480
+ value: settings.abandoned_cart_discount_type || "percentage",
481
+ onValueChange: (v) => {
482
+ if (v !== (settings.abandoned_cart_discount_type || "percentage")) update("abandoned_cart_discount_type", v);
483
+ },
484
+ children: [
485
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "Select type" }) }),
486
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
487
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "percentage", children: "Percentage (%)" }),
488
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "fixed", children: "Fixed Amount" })
489
+ ] })
490
+ ]
491
+ }
492
+ )
493
+ ] }),
494
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
495
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Discount Value" }),
496
+ /* @__PURE__ */ jsxRuntime.jsx(
497
+ ui.Input,
498
+ {
499
+ type: "number",
500
+ min: 1,
501
+ value: settings.abandoned_cart_discount_value || 10,
502
+ onChange: (e) => update("abandoned_cart_discount_value", parseInt(e.target.value, 10) || 10)
503
+ }
504
+ )
505
+ ] }),
506
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
507
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Max Uses Per Code" }),
508
+ /* @__PURE__ */ jsxRuntime.jsx(
509
+ ui.Input,
510
+ {
511
+ type: "number",
512
+ min: 1,
513
+ value: settings.abandoned_cart_discount_max_uses || 1,
514
+ onChange: (e) => update("abandoned_cart_discount_max_uses", parseInt(e.target.value, 10) || 1)
515
+ }
516
+ )
517
+ ] }),
518
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
519
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Expires After (hours)" }),
520
+ /* @__PURE__ */ jsxRuntime.jsx(
521
+ ui.Input,
522
+ {
523
+ type: "number",
524
+ min: 1,
525
+ value: settings.abandoned_cart_discount_expires_hours || 48,
526
+ onChange: (e) => update("abandoned_cart_discount_expires_hours", parseInt(e.target.value, 10) || 48)
527
+ }
528
+ )
529
+ ] }),
530
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
531
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Code Prefix" }),
532
+ /* @__PURE__ */ jsxRuntime.jsx(
533
+ ui.Input,
534
+ {
535
+ placeholder: "COMEBACK",
536
+ value: settings.abandoned_cart_discount_code_prefix || "COMEBACK",
537
+ onChange: (e) => update("abandoned_cart_discount_code_prefix", e.target.value.toUpperCase())
538
+ }
539
+ ),
540
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs mt-1", children: "Code format: PREFIX-8CHARS (e.g. COMEBACK-B7TK9W3P)" })
541
+ ] }),
542
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "col-span-2", children: [
543
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Discount Email Template ID (optional)" }),
544
+ /* @__PURE__ */ jsxRuntime.jsx(
545
+ ui.Input,
546
+ {
547
+ placeholder: "Leave blank to use abandoned cart template",
548
+ value: settings.abandoned_cart_discount_template_id || "",
549
+ onChange: (e) => update("abandoned_cart_discount_template_id", e.target.value)
550
+ }
551
+ ),
552
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs mt-1", children: "Separate template for the discount email. Falls back to abandoned cart template." })
553
+ ] }),
554
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "col-span-2", children: [
555
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Excluded Currencies" }),
556
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs mb-2", children: "Discount will NOT apply to orders in these currencies. Click to toggle." }),
557
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-1", children: [
558
+ currencies.map((cur) => {
559
+ const excluded = Array.isArray(settings.abandoned_cart_discount_excluded_currencies) ? settings.abandoned_cart_discount_excluded_currencies : [];
560
+ const isExcluded = excluded.includes(cur);
561
+ return /* @__PURE__ */ jsxRuntime.jsxs(
562
+ "button",
563
+ {
564
+ type: "button",
565
+ className: `px-2 py-1 rounded text-xs font-mono border transition-colors ${isExcluded ? "bg-red-100 border-red-300 text-red-700" : "bg-ui-bg-base border-ui-border-base text-ui-fg-subtle hover:bg-ui-bg-base-hover"}`,
566
+ onClick: () => {
567
+ const next = isExcluded ? excluded.filter((c) => c !== cur) : [...excluded, cur];
568
+ update("abandoned_cart_discount_excluded_currencies", next);
569
+ },
570
+ children: [
571
+ cur.toUpperCase(),
572
+ isExcluded ? " ✕" : ""
573
+ ]
574
+ },
575
+ cur
576
+ );
577
+ }),
578
+ currencies.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs", children: "No currencies found in store." })
579
+ ] })
580
+ ] })
581
+ ] })
582
+ ] })
583
+ ] }) }),
584
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
585
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Brevo Contact Sync" }),
586
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mb-4", children: "Sync customers to Brevo Contacts when they register or place an order." }),
587
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 p-3 border rounded-lg", children: [
588
+ /* @__PURE__ */ jsxRuntime.jsx(
589
+ ui.Switch,
590
+ {
591
+ checked: settings.contact_sync_enabled,
592
+ onCheckedChange: (v) => update("contact_sync_enabled", v)
593
+ }
594
+ ),
595
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Enable Contact Sync" }) }),
596
+ /* @__PURE__ */ jsxRuntime.jsx(
597
+ ui.Input,
598
+ {
599
+ className: "w-40",
600
+ placeholder: "Brevo List ID (opt)",
601
+ type: "number",
602
+ value: settings.contact_sync_list_id || "",
603
+ onChange: (e) => update("contact_sync_list_id", parseInt(e.target.value, 10) || null)
604
+ }
605
+ )
606
+ ] })
607
+ ] }) }),
608
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
609
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Event Tracking" }),
610
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mb-4", children: "Send events to Brevo for automation triggers (order_placed, customer_created, etc.)" }),
611
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 p-3 border rounded-lg", children: [
612
+ /* @__PURE__ */ jsxRuntime.jsx(
613
+ ui.Switch,
614
+ {
615
+ checked: settings.event_tracking_enabled,
616
+ onCheckedChange: (v) => update("event_tracking_enabled", v)
617
+ }
618
+ ),
619
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Enable Event Tracking" }) })
620
+ ] })
621
+ ] }) }),
622
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
623
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Review Request" }),
624
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 mb-4 p-3 border rounded-lg", children: [
625
+ /* @__PURE__ */ jsxRuntime.jsx(
626
+ ui.Switch,
627
+ {
628
+ checked: settings.review_request_enabled,
629
+ onCheckedChange: (v) => update("review_request_enabled", v)
630
+ }
631
+ ),
632
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Send Review Request Email" }) }),
633
+ /* @__PURE__ */ jsxRuntime.jsx(
634
+ ui.Input,
635
+ {
636
+ className: "w-32",
637
+ placeholder: "Template ID",
638
+ value: settings.review_request_template_id || "",
639
+ onChange: (e) => update("review_request_template_id", e.target.value)
640
+ }
641
+ )
642
+ ] }),
643
+ settings.review_request_enabled && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-md ml-4", children: [
644
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Days After Order" }),
645
+ /* @__PURE__ */ jsxRuntime.jsx(
646
+ ui.Input,
647
+ {
648
+ type: "number",
649
+ min: 1,
650
+ max: 60,
651
+ value: settings.review_request_days_after || 7,
652
+ onChange: (e) => update("review_request_days_after", parseInt(e.target.value, 10) || 7)
653
+ }
654
+ ),
655
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs mt-1", children: "Send review request X days after order placed." })
656
+ ] })
657
+ ] }) }),
658
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
659
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Win-back Campaign" }),
660
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 mb-4 p-3 border rounded-lg", children: [
661
+ /* @__PURE__ */ jsxRuntime.jsx(
662
+ ui.Switch,
663
+ {
664
+ checked: settings.winback_enabled,
665
+ onCheckedChange: (v) => update("winback_enabled", v)
666
+ }
667
+ ),
668
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Enable Win-back Emails" }) }),
669
+ /* @__PURE__ */ jsxRuntime.jsx(
670
+ ui.Input,
671
+ {
672
+ className: "w-32",
673
+ placeholder: "Template ID",
674
+ value: settings.winback_template_id || "",
675
+ onChange: (e) => update("winback_template_id", e.target.value)
676
+ }
677
+ )
678
+ ] }),
679
+ settings.winback_enabled && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-md ml-4", children: [
680
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Days Inactive" }),
681
+ /* @__PURE__ */ jsxRuntime.jsx(
682
+ ui.Input,
683
+ {
684
+ type: "number",
685
+ min: 7,
686
+ max: 365,
687
+ value: settings.winback_days_inactive || 30,
688
+ onChange: (e) => update("winback_days_inactive", parseInt(e.target.value, 10) || 30)
689
+ }
690
+ ),
691
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs mt-1", children: "Send win-back email to customers inactive for X days." })
692
+ ] })
693
+ ] }) }),
694
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
695
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Multi-language Templates" }),
696
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 mb-4 p-3 border rounded-lg", children: [
697
+ /* @__PURE__ */ jsxRuntime.jsx(
698
+ ui.Switch,
699
+ {
700
+ checked: settings.multilang_enabled,
701
+ onCheckedChange: (v) => update("multilang_enabled", v)
702
+ }
703
+ ),
704
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Enable Multi-language" }) })
705
+ ] }),
706
+ settings.multilang_enabled && (() => {
707
+ const EVENT_OPTIONS = [
708
+ { value: "order.placed", label: "Order Placed" },
709
+ { value: "order.canceled", label: "Order Canceled" },
710
+ { value: "order.delivered", label: "Order Delivered" },
711
+ { value: "customer.created", label: "Customer Created" },
712
+ { value: "shipment.confirmed", label: "Shipment Confirmed" },
713
+ { value: "cart.abandoned", label: "Cart Abandoned" },
714
+ { value: "cart.abandoned.discount", label: "Cart Abandoned (Discount)" },
715
+ { value: "review.request", label: "Review Request" },
716
+ { value: "winback", label: "Win-back" }
717
+ ];
718
+ const templates = settings.multilang_templates || {};
719
+ const locales = Object.keys(templates);
720
+ const addLocale = (code) => {
721
+ const key = code.trim().toLowerCase();
722
+ if (!key || templates[key]) return;
723
+ update("multilang_templates", { ...templates, [key]: {} });
724
+ };
725
+ const removeLocale = (locale) => {
726
+ const next = { ...templates };
727
+ delete next[locale];
728
+ update("multilang_templates", next);
729
+ };
730
+ const setTemplate = (locale, event, value) => {
731
+ const localeMap = { ...templates[locale] || {} };
732
+ if (value.trim() === "") {
733
+ delete localeMap[event];
734
+ } else {
735
+ localeMap[event] = value;
736
+ }
737
+ update("multilang_templates", { ...templates, [locale]: localeMap });
738
+ };
739
+ const addEventToLocale = (locale) => {
740
+ const usedEvents = Object.keys(templates[locale] || {});
741
+ const available = EVENT_OPTIONS.filter((e) => !usedEvents.includes(e.value));
742
+ if (!available.length) return;
743
+ const localeMap = { ...templates[locale] || {}, [available[0].value]: "" };
744
+ update("multilang_templates", { ...templates, [locale]: localeMap });
745
+ };
746
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-4 space-y-4", children: [
747
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { className: "text-ui-fg-subtle text-sm", children: [
748
+ "Override template IDs per language. Customer locale is set via ",
749
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "customer.metadata.preferred_locale" }),
750
+ " from storefront. If no match → uses default template ID above."
751
+ ] }),
752
+ locales.map((locale) => {
753
+ const events = templates[locale] || {};
754
+ const eventKeys = Object.keys(events);
755
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border rounded-lg p-4", children: [
756
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-3", children: [
757
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
758
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: "blue", className: "text-sm font-mono", children: locale.toUpperCase() }),
759
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { className: "text-ui-fg-subtle text-xs", children: [
760
+ eventKeys.length,
761
+ " template(s)"
762
+ ] })
763
+ ] }),
764
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
765
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: () => addEventToLocale(locale), children: "+ Event" }),
766
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "danger", size: "small", onClick: () => removeLocale(locale), children: "Remove" })
767
+ ] })
768
+ ] }),
769
+ eventKeys.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs italic", children: 'No templates configured. Click "+ Event" to add.' }),
770
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: eventKeys.map((event) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
771
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsxs(
772
+ ui.Select,
773
+ {
774
+ value: event,
775
+ onValueChange: (newEvent) => {
776
+ if (newEvent === event) return;
777
+ const val = events[event];
778
+ const next = { ...events };
779
+ delete next[event];
780
+ next[newEvent] = val;
781
+ update("multilang_templates", { ...templates, [locale]: next });
782
+ },
783
+ children: [
784
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
785
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Content, { children: EVENT_OPTIONS.map((opt) => /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: opt.value, children: opt.label }, opt.value)) })
786
+ ]
787
+ }
788
+ ) }),
789
+ /* @__PURE__ */ jsxRuntime.jsx(
790
+ ui.Input,
791
+ {
792
+ className: "w-28",
793
+ placeholder: "Template ID",
794
+ value: events[event] || "",
795
+ onChange: (e) => setTemplate(locale, event, e.target.value)
796
+ }
797
+ ),
798
+ /* @__PURE__ */ jsxRuntime.jsx(
799
+ ui.Button,
800
+ {
801
+ variant: "danger",
802
+ size: "small",
803
+ onClick: () => {
804
+ const next = { ...templates[locale] || {} };
805
+ delete next[event];
806
+ update("multilang_templates", { ...templates, [locale]: next });
807
+ },
808
+ children: "×"
809
+ }
810
+ )
811
+ ] }, event)) })
812
+ ] }, locale);
813
+ }),
814
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
815
+ /* @__PURE__ */ jsxRuntime.jsx(
816
+ ui.Input,
817
+ {
818
+ className: "w-28",
819
+ placeholder: "vi, en, zh...",
820
+ id: "_newLocaleInput",
821
+ onKeyDown: (e) => {
822
+ if (e.key === "Enter") {
823
+ addLocale(e.target.value);
824
+ e.target.value = "";
825
+ }
826
+ }
827
+ }
828
+ ),
829
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => {
830
+ const input = document.getElementById("_newLocaleInput");
831
+ if (input) {
832
+ addLocale(input.value);
833
+ input.value = "";
834
+ }
835
+ }, children: "+ Add Language" })
836
+ ] })
837
+ ] });
838
+ })()
839
+ ] }) }),
840
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
841
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Webhook" }),
842
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mb-4", children: "Receive Brevo email events (delivered, opened, clicked, bounced)." }),
843
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 mb-4 p-3 border rounded-lg", children: [
844
+ /* @__PURE__ */ jsxRuntime.jsx(
845
+ ui.Switch,
846
+ {
847
+ checked: settings.webhook_enabled,
848
+ onCheckedChange: (v) => update("webhook_enabled", v)
849
+ }
850
+ ),
851
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Enable Webhook" }) })
852
+ ] }),
853
+ settings.webhook_enabled && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-4 max-w-lg ml-4", children: [
854
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
855
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Webhook URL" }),
856
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { readOnly: true, value: `${window.location.origin}/hooks/brevo-webhook` }),
857
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs mt-1", children: "Configure this URL in Brevo → Settings → Webhooks." })
858
+ ] }),
859
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
860
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Webhook Secret (optional)" }),
861
+ /* @__PURE__ */ jsxRuntime.jsx(
862
+ ui.Input,
863
+ {
864
+ placeholder: "Optional secret for verification",
865
+ value: settings.webhook_secret || "",
866
+ onChange: (e) => update("webhook_secret", e.target.value)
867
+ }
868
+ )
869
+ ] })
870
+ ] })
871
+ ] }) }),
872
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
873
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "WhatsApp Notifications" }),
874
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 mb-4 p-3 border rounded-lg", children: [
875
+ /* @__PURE__ */ jsxRuntime.jsx(
876
+ ui.Switch,
877
+ {
878
+ checked: settings.whatsapp_enabled,
879
+ onCheckedChange: (v) => update("whatsapp_enabled", v)
880
+ }
881
+ ),
882
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Enable WhatsApp" }) })
883
+ ] }),
884
+ settings.whatsapp_enabled && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-4 max-w-md ml-4", children: [
885
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
886
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Order Placed Template" }),
887
+ /* @__PURE__ */ jsxRuntime.jsx(
888
+ ui.Input,
889
+ {
890
+ placeholder: "Brevo WhatsApp template name",
891
+ value: settings.whatsapp_order_placed_template || "",
892
+ onChange: (e) => update("whatsapp_order_placed_template", e.target.value)
893
+ }
894
+ )
895
+ ] }),
896
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
897
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Shipment Template" }),
898
+ /* @__PURE__ */ jsxRuntime.jsx(
899
+ ui.Input,
900
+ {
901
+ placeholder: "Brevo WhatsApp template name",
902
+ value: settings.whatsapp_shipment_template || "",
903
+ onChange: (e) => update("whatsapp_shipment_template", e.target.value)
904
+ }
905
+ )
906
+ ] }),
907
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
908
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Abandoned Cart Template" }),
909
+ /* @__PURE__ */ jsxRuntime.jsx(
910
+ ui.Input,
911
+ {
912
+ placeholder: "Brevo WhatsApp template name",
913
+ value: settings.whatsapp_abandoned_cart_template || "",
914
+ onChange: (e) => update("whatsapp_abandoned_cart_template", e.target.value)
915
+ }
916
+ )
917
+ ] })
918
+ ] })
919
+ ] }) }),
920
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0 mt-4 mb-8", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
921
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "SMS Notifications" }),
922
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 mb-4 p-3 border rounded-lg", children: [
923
+ /* @__PURE__ */ jsxRuntime.jsx(
924
+ ui.Switch,
925
+ {
926
+ checked: settings.sms_enabled,
927
+ onCheckedChange: (v) => update("sms_enabled", v)
928
+ }
929
+ ),
930
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Enable SMS" }) })
931
+ ] }),
932
+ settings.sms_enabled && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-4 max-w-md ml-4", children: [
933
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
934
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "SMS Sender Name" }),
935
+ /* @__PURE__ */ jsxRuntime.jsx(
936
+ ui.Input,
937
+ {
938
+ placeholder: "MyStore",
939
+ value: settings.sms_sender || "",
940
+ onChange: (e) => update("sms_sender", e.target.value)
941
+ }
942
+ ),
943
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle text-xs mt-1", children: "Max 11 chars, alphanumeric." })
944
+ ] }),
945
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
946
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Order Placed SMS Content" }),
947
+ /* @__PURE__ */ jsxRuntime.jsx(
948
+ ui.Input,
949
+ {
950
+ placeholder: "Your order #{{display_id}} has been placed!",
951
+ value: settings.sms_order_placed_content || "",
952
+ onChange: (e) => update("sms_order_placed_content", e.target.value)
953
+ }
954
+ )
955
+ ] }),
956
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
957
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Shipment SMS Content" }),
958
+ /* @__PURE__ */ jsxRuntime.jsx(
959
+ ui.Input,
960
+ {
961
+ placeholder: "Your order #{{display_id}} has been shipped!",
962
+ value: settings.sms_shipment_content || "",
963
+ onChange: (e) => update("sms_shipment_content", e.target.value)
964
+ }
965
+ )
966
+ ] })
967
+ ] })
968
+ ] }) })
969
+ ] });
970
+ };
971
+ const config = adminSdk.defineRouteConfig({
972
+ label: "Brevo",
973
+ icon: icons.EnvelopeSolid
974
+ });
2
975
  const widgetModule = { widgets: [] };
3
976
  const routeModule = {
4
- routes: []
977
+ routes: [
978
+ {
979
+ Component: BrevoAnalyticsPage,
980
+ path: "/brevo-analytics"
981
+ },
982
+ {
983
+ Component: BrevoSettingsPage,
984
+ path: "/settings/brevo"
985
+ }
986
+ ]
5
987
  };
6
988
  const menuItemModule = {
7
- menuItems: []
989
+ menuItems: [
990
+ {
991
+ label: config$1.label,
992
+ icon: config$1.icon,
993
+ path: "/brevo-analytics",
994
+ nested: void 0,
995
+ rank: void 0,
996
+ translationNs: void 0
997
+ },
998
+ {
999
+ label: config.label,
1000
+ icon: config.icon,
1001
+ path: "/settings/brevo",
1002
+ nested: void 0,
1003
+ rank: void 0,
1004
+ translationNs: void 0
1005
+ }
1006
+ ]
8
1007
  };
9
1008
  const formModule = { customFields: {} };
10
1009
  const displayModule = {
11
1010
  displays: {}
12
1011
  };
1012
+ const i18nModule = { resources: {} };
13
1013
  const plugin = {
14
1014
  widgetModule,
15
1015
  routeModule,
16
1016
  menuItemModule,
17
1017
  formModule,
18
- displayModule
1018
+ displayModule,
1019
+ i18nModule
19
1020
  };
20
1021
  module.exports = plugin;