@sam-ael/medusa-plugin-whatsapp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,581 @@
1
+ "use strict";
2
+ const jsxRuntime = require("react/jsx-runtime");
3
+ const adminSdk = require("@medusajs/admin-sdk");
4
+ const icons = require("@medusajs/icons");
5
+ const ui = require("@medusajs/ui");
6
+ const react = require("react");
7
+ const MEDUSA_EVENTS = [
8
+ "order.placed",
9
+ "order.completed",
10
+ "order.canceled",
11
+ "order.updated",
12
+ "order.fulfillment_created",
13
+ "fulfillment.created",
14
+ "fulfillment.shipment_created",
15
+ "fulfillment.delivery_created",
16
+ "customer.created",
17
+ "customer.updated",
18
+ "return.created",
19
+ "return.received",
20
+ "claim.created",
21
+ "exchange.created"
22
+ ];
23
+ const BACKEND_URL = __BACKEND_URL__ ?? "";
24
+ async function api(path, options) {
25
+ const res = await fetch(`${BACKEND_URL}/admin/whatsapp${path}`, {
26
+ ...options,
27
+ credentials: "include",
28
+ headers: {
29
+ "Content-Type": "application/json",
30
+ ...options == null ? void 0 : options.headers
31
+ }
32
+ });
33
+ return res.json();
34
+ }
35
+ function ConfigSection() {
36
+ const [config2, setConfig] = react.useState(null);
37
+ const [loading, setLoading] = react.useState(true);
38
+ const [saving, setSaving] = react.useState(false);
39
+ const [form, setForm] = react.useState({
40
+ phone_number_id: "",
41
+ access_token: "",
42
+ api_version: "v21.0",
43
+ default_language_code: "en_US",
44
+ active: true
45
+ });
46
+ const loadConfig = react.useCallback(async () => {
47
+ setLoading(true);
48
+ const data = await api("/config");
49
+ if (data.config) {
50
+ setConfig(data.config);
51
+ setForm({
52
+ phone_number_id: data.config.phone_number_id || "",
53
+ access_token: "",
54
+ api_version: data.config.api_version || "v21.0",
55
+ default_language_code: data.config.default_language_code || "en_US",
56
+ active: data.config.active ?? true
57
+ });
58
+ }
59
+ setLoading(false);
60
+ }, []);
61
+ react.useEffect(() => {
62
+ loadConfig();
63
+ }, [loadConfig]);
64
+ const saveConfig = async () => {
65
+ setSaving(true);
66
+ const payload = { ...form };
67
+ if (!payload.access_token) delete payload.access_token;
68
+ await api("/config", { method: "POST", body: JSON.stringify(payload) });
69
+ ui.toast.success("Configuration saved");
70
+ await loadConfig();
71
+ setSaving(false);
72
+ };
73
+ if (loading) return /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading configuration..." });
74
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-6", children: [
75
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-4", children: [
76
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "WhatsApp Configuration" }),
77
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
78
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "wa-active", children: "Active" }),
79
+ /* @__PURE__ */ jsxRuntime.jsx(
80
+ ui.Switch,
81
+ {
82
+ id: "wa-active",
83
+ checked: form.active,
84
+ onCheckedChange: (checked) => setForm({ ...form, active: checked })
85
+ }
86
+ )
87
+ ] })
88
+ ] }),
89
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
90
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
91
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "phone-id", children: "Phone Number ID" }),
92
+ /* @__PURE__ */ jsxRuntime.jsx(
93
+ ui.Input,
94
+ {
95
+ id: "phone-id",
96
+ placeholder: "e.g. 1234567890",
97
+ value: form.phone_number_id,
98
+ onChange: (e) => setForm({ ...form, phone_number_id: e.target.value })
99
+ }
100
+ )
101
+ ] }),
102
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
103
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { htmlFor: "access-token", children: [
104
+ "Access Token ",
105
+ config2 && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: "(leave blank to keep current)" })
106
+ ] }),
107
+ /* @__PURE__ */ jsxRuntime.jsx(
108
+ ui.Input,
109
+ {
110
+ id: "access-token",
111
+ type: "password",
112
+ placeholder: config2 ? "••••••••" : "Bearer token",
113
+ value: form.access_token,
114
+ onChange: (e) => setForm({ ...form, access_token: e.target.value })
115
+ }
116
+ )
117
+ ] }),
118
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
119
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "api-version", children: "API Version" }),
120
+ /* @__PURE__ */ jsxRuntime.jsx(
121
+ ui.Input,
122
+ {
123
+ id: "api-version",
124
+ placeholder: "v21.0",
125
+ value: form.api_version,
126
+ onChange: (e) => setForm({ ...form, api_version: e.target.value })
127
+ }
128
+ )
129
+ ] }),
130
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
131
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "lang-code", children: "Default Language Code" }),
132
+ /* @__PURE__ */ jsxRuntime.jsx(
133
+ ui.Input,
134
+ {
135
+ id: "lang-code",
136
+ placeholder: "en_US",
137
+ value: form.default_language_code,
138
+ onChange: (e) => setForm({ ...form, default_language_code: e.target.value })
139
+ }
140
+ )
141
+ ] })
142
+ ] }),
143
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4 flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: saveConfig, isLoading: saving, children: "Save Configuration" }) }),
144
+ config2 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-muted", children: [
145
+ "Current token: ",
146
+ config2.access_token
147
+ ] }) })
148
+ ] });
149
+ }
150
+ const RECIPIENT_OPTIONS = [
151
+ { value: "billing_shipping", label: "Billing phone (fallback: shipping)" },
152
+ { value: "billing", label: "Billing phone only" },
153
+ { value: "shipping", label: "Shipping phone only" },
154
+ { value: "custom", label: "Custom phone number" }
155
+ ];
156
+ const ORDER_DATA_PATHS = [
157
+ { value: "order.display_id", label: "Order ID" },
158
+ { value: "order.total", label: "Order Total" },
159
+ { value: "order.currency_code", label: "Currency Code" },
160
+ { value: "order.email", label: "Order Email" },
161
+ { value: "order.shipping_address.first_name", label: "Shipping First Name" },
162
+ { value: "order.shipping_address.last_name", label: "Shipping Last Name" },
163
+ { value: "order.shipping_address.phone", label: "Shipping Phone" },
164
+ { value: "order.shipping_address.city", label: "Shipping City" },
165
+ { value: "order.billing_address.first_name", label: "Billing First Name" },
166
+ { value: "order.billing_address.last_name", label: "Billing Last Name" },
167
+ { value: "order.billing_address.phone", label: "Billing Phone" },
168
+ { value: "order.customer.first_name", label: "Customer First Name" },
169
+ { value: "order.customer.last_name", label: "Customer Last Name" },
170
+ { value: "order.customer.email", label: "Customer Email" },
171
+ { value: "order.customer.phone", label: "Customer Phone" }
172
+ ];
173
+ const EMPTY_VAR = { name: "", path: "" };
174
+ const MAX_VARS = 5;
175
+ function MappingsSection() {
176
+ const [mappings, setMappings] = react.useState([]);
177
+ const [loading, setLoading] = react.useState(true);
178
+ const [showForm, setShowForm] = react.useState(false);
179
+ const [editingId, setEditingId] = react.useState(null);
180
+ const [templateVars, setTemplateVars] = react.useState([{ ...EMPTY_VAR }]);
181
+ const [form, setForm] = react.useState({
182
+ event_name: MEDUSA_EVENTS[0],
183
+ template_name: "",
184
+ language_code: "en_US",
185
+ recipient_type: "billing_shipping",
186
+ recipient_phone: "",
187
+ active: true
188
+ });
189
+ const loadMappings = react.useCallback(async () => {
190
+ setLoading(true);
191
+ const data = await api("/mappings");
192
+ setMappings(data.mappings || []);
193
+ setLoading(false);
194
+ }, []);
195
+ react.useEffect(() => {
196
+ loadMappings();
197
+ }, [loadMappings]);
198
+ const resetForm = () => {
199
+ setForm({ event_name: MEDUSA_EVENTS[0], template_name: "", language_code: "en_US", recipient_type: "billing_shipping", recipient_phone: "", active: true });
200
+ setTemplateVars([{ ...EMPTY_VAR }]);
201
+ setEditingId(null);
202
+ setShowForm(false);
203
+ };
204
+ const updateVar = (index, field, value) => {
205
+ setTemplateVars((prev) => prev.map((v, i) => i === index ? { ...v, [field]: value } : v));
206
+ };
207
+ const addVar = () => {
208
+ if (templateVars.length < MAX_VARS) setTemplateVars((prev) => [...prev, { ...EMPTY_VAR }]);
209
+ };
210
+ const removeVar = (index) => {
211
+ setTemplateVars((prev) => prev.length <= 1 ? [{ ...EMPTY_VAR }] : prev.filter((_, i) => i !== index));
212
+ };
213
+ const saveMapping = async () => {
214
+ const variables = {};
215
+ templateVars.forEach((v) => {
216
+ if (v.name.trim() && v.path.trim()) variables[v.name.trim()] = v.path.trim();
217
+ });
218
+ const payload = { ...form, template_variables: variables };
219
+ if (editingId) {
220
+ await api(`/mappings/${editingId}`, { method: "PUT", body: JSON.stringify(payload) });
221
+ ui.toast.success("Mapping updated");
222
+ } else {
223
+ await api("/mappings", { method: "POST", body: JSON.stringify(payload) });
224
+ ui.toast.success("Mapping created");
225
+ }
226
+ resetForm();
227
+ await loadMappings();
228
+ };
229
+ const deleteMapping = async (id) => {
230
+ await api(`/mappings/${id}`, { method: "DELETE" });
231
+ ui.toast.success("Mapping deleted");
232
+ await loadMappings();
233
+ };
234
+ const editMapping = (m) => {
235
+ const vars = m.template_variables || {};
236
+ const rows = Object.entries(vars).map(([name, path]) => ({ name, path }));
237
+ setTemplateVars(rows.length > 0 ? rows : [{ ...EMPTY_VAR }]);
238
+ setForm({
239
+ event_name: m.event_name,
240
+ template_name: m.template_name,
241
+ language_code: m.language_code,
242
+ recipient_type: m.recipient_type || "billing_shipping",
243
+ recipient_phone: m.recipient_phone || "",
244
+ active: m.active
245
+ });
246
+ setEditingId(m.id);
247
+ setShowForm(true);
248
+ };
249
+ const recipientLabel = (type, phone) => {
250
+ const opt = RECIPIENT_OPTIONS.find((o) => o.value === type);
251
+ if (type === "custom" && phone) return `Custom: ${phone}`;
252
+ return (opt == null ? void 0 : opt.label) || type;
253
+ };
254
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-6", children: [
255
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-4", children: [
256
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Event → Template Mappings" }),
257
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => {
258
+ resetForm();
259
+ setShowForm(!showForm);
260
+ }, children: showForm ? "Cancel" : "Add Mapping" })
261
+ ] }),
262
+ showForm && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border rounded-lg p-4 mb-4 bg-ui-bg-subtle", children: [
263
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
264
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
265
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "event-name", children: "Medusa Event" }),
266
+ /* @__PURE__ */ jsxRuntime.jsx(
267
+ ui.Input,
268
+ {
269
+ id: "event-name",
270
+ list: "medusa-events-list",
271
+ placeholder: "Select or type a custom event...",
272
+ value: form.event_name,
273
+ onChange: (e) => setForm({ ...form, event_name: e.target.value })
274
+ }
275
+ ),
276
+ /* @__PURE__ */ jsxRuntime.jsx("datalist", { id: "medusa-events-list", children: MEDUSA_EVENTS.map((e) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: e }, e)) }),
277
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted mt-1", children: "Pick a preset or type any custom event name" })
278
+ ] }),
279
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
280
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "tpl-name", children: "WhatsApp Template Name" }),
281
+ /* @__PURE__ */ jsxRuntime.jsx(
282
+ ui.Input,
283
+ {
284
+ id: "tpl-name",
285
+ placeholder: "e.g. order_confirmation",
286
+ value: form.template_name,
287
+ onChange: (e) => setForm({ ...form, template_name: e.target.value })
288
+ }
289
+ )
290
+ ] }),
291
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
292
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "tpl-lang", children: "Language Code" }),
293
+ /* @__PURE__ */ jsxRuntime.jsx(
294
+ ui.Input,
295
+ {
296
+ id: "tpl-lang",
297
+ placeholder: "en_US",
298
+ value: form.language_code,
299
+ onChange: (e) => setForm({ ...form, language_code: e.target.value })
300
+ }
301
+ )
302
+ ] }),
303
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
304
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "recipient-type", children: "Send To" }),
305
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: form.recipient_type, onValueChange: (val) => setForm({ ...form, recipient_type: val }), children: [
306
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "Select recipient" }) }),
307
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Content, { children: RECIPIENT_OPTIONS.map((opt) => /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: opt.value, children: opt.label }, opt.value)) })
308
+ ] })
309
+ ] }),
310
+ form.recipient_type === "custom" && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
311
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "recipient-phone", children: "Custom Phone Number" }),
312
+ /* @__PURE__ */ jsxRuntime.jsx(
313
+ ui.Input,
314
+ {
315
+ id: "recipient-phone",
316
+ placeholder: "e.g. 919876543210",
317
+ value: form.recipient_phone,
318
+ onChange: (e) => setForm({ ...form, recipient_phone: e.target.value })
319
+ }
320
+ ),
321
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted mt-1", children: "Include country code, no + prefix" })
322
+ ] }),
323
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-end gap-2", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
324
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "mapping-active", children: "Active" }),
325
+ /* @__PURE__ */ jsxRuntime.jsx(
326
+ ui.Switch,
327
+ {
328
+ id: "mapping-active",
329
+ checked: form.active,
330
+ onCheckedChange: (checked) => setForm({ ...form, active: checked })
331
+ }
332
+ )
333
+ ] }) })
334
+ ] }),
335
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4", children: [
336
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
337
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Template Variables" }),
338
+ templateVars.length < MAX_VARS && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: addVar, children: "+ Add Variable" })
339
+ ] }),
340
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted mb-2", children: "Map WhatsApp template variable positions (e.g. 1, 2, 3) to order data fields." }),
341
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-2", children: templateVars.map((v, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
342
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-24 flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
343
+ ui.Input,
344
+ {
345
+ placeholder: `${i + 1}`,
346
+ value: v.name,
347
+ onChange: (e) => updateVar(i, "name", e.target.value)
348
+ }
349
+ ) }),
350
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(
351
+ ui.Input,
352
+ {
353
+ list: "order-data-paths",
354
+ placeholder: "Select or type a data path...",
355
+ value: v.path,
356
+ onChange: (e) => updateVar(i, "path", e.target.value)
357
+ }
358
+ ) }),
359
+ /* @__PURE__ */ jsxRuntime.jsx(
360
+ ui.Button,
361
+ {
362
+ variant: "secondary",
363
+ size: "small",
364
+ onClick: () => removeVar(i),
365
+ children: "✕"
366
+ }
367
+ )
368
+ ] }, i)) }),
369
+ /* @__PURE__ */ jsxRuntime.jsx("datalist", { id: "order-data-paths", children: ORDER_DATA_PATHS.map((p) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: p.value, label: p.label }, p.value)) })
370
+ ] }),
371
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3 flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: saveMapping, children: editingId ? "Update Mapping" : "Create Mapping" }) })
372
+ ] }),
373
+ loading ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading mappings..." }) : mappings.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted", children: "No event mappings configured. Add one to get started." }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
374
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
375
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Event" }),
376
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Template" }),
377
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Send To" }),
378
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Language" }),
379
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
380
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "text-right", children: "Actions" })
381
+ ] }) }),
382
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: mappings.map((m) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
383
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: "blue", children: m.event_name }) }),
384
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: m.template_name }),
385
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: recipientLabel(m.recipient_type, m.recipient_phone) }) }),
386
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: m.language_code }),
387
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: m.active ? "green" : "grey", children: m.active ? "Active" : "Inactive" }) }),
388
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-2", children: [
389
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: () => editMapping(m), children: "Edit" }),
390
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "danger", size: "small", onClick: () => deleteMapping(m.id), children: "Delete" })
391
+ ] }) })
392
+ ] }, m.id)) })
393
+ ] })
394
+ ] });
395
+ }
396
+ function TestSection() {
397
+ const [sending, setSending] = react.useState(false);
398
+ const [form, setForm] = react.useState({
399
+ phone_number: "",
400
+ template_name: "",
401
+ language_code: "en_US",
402
+ template_variables: "{}"
403
+ });
404
+ const sendTest = async () => {
405
+ var _a, _b;
406
+ if (!form.phone_number || !form.template_name) {
407
+ ui.toast.error("Phone number and template name are required");
408
+ return;
409
+ }
410
+ setSending(true);
411
+ let variables = {};
412
+ try {
413
+ variables = JSON.parse(form.template_variables);
414
+ } catch {
415
+ }
416
+ const result = await api("/test", {
417
+ method: "POST",
418
+ body: JSON.stringify({ ...form, template_variables: variables })
419
+ });
420
+ if (result.success) {
421
+ ui.toast.success("Test message sent!");
422
+ } else {
423
+ ui.toast.error(`Failed: ${((_b = (_a = result.error) == null ? void 0 : _a.error) == null ? void 0 : _b.message) || result.error || "Unknown error"}`);
424
+ }
425
+ setSending(false);
426
+ };
427
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-6", children: [
428
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4", children: "Send Test Message" }),
429
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
430
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
431
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "test-phone", children: "Phone Number (with country code)" }),
432
+ /* @__PURE__ */ jsxRuntime.jsx(
433
+ ui.Input,
434
+ {
435
+ id: "test-phone",
436
+ placeholder: "e.g. 919876543210",
437
+ value: form.phone_number,
438
+ onChange: (e) => setForm({ ...form, phone_number: e.target.value })
439
+ }
440
+ )
441
+ ] }),
442
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
443
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "test-tpl", children: "Template Name" }),
444
+ /* @__PURE__ */ jsxRuntime.jsx(
445
+ ui.Input,
446
+ {
447
+ id: "test-tpl",
448
+ placeholder: "e.g. hello_world",
449
+ value: form.template_name,
450
+ onChange: (e) => setForm({ ...form, template_name: e.target.value })
451
+ }
452
+ )
453
+ ] }),
454
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
455
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "test-lang", children: "Language Code" }),
456
+ /* @__PURE__ */ jsxRuntime.jsx(
457
+ ui.Input,
458
+ {
459
+ id: "test-lang",
460
+ placeholder: "en_US",
461
+ value: form.language_code,
462
+ onChange: (e) => setForm({ ...form, language_code: e.target.value })
463
+ }
464
+ )
465
+ ] }),
466
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
467
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "test-vars", children: "Template Variables (JSON)" }),
468
+ /* @__PURE__ */ jsxRuntime.jsx(
469
+ ui.Input,
470
+ {
471
+ id: "test-vars",
472
+ placeholder: '{"1": "John"}',
473
+ value: form.template_variables,
474
+ onChange: (e) => setForm({ ...form, template_variables: e.target.value })
475
+ }
476
+ )
477
+ ] })
478
+ ] }),
479
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4 flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: sendTest, isLoading: sending, children: "Send Test Message" }) })
480
+ ] });
481
+ }
482
+ function LogsSection() {
483
+ const [logs, setLogs] = react.useState([]);
484
+ const [loading, setLoading] = react.useState(true);
485
+ const loadLogs = react.useCallback(async () => {
486
+ setLoading(true);
487
+ const data = await api("/logs?limit=20");
488
+ setLogs(data.logs || []);
489
+ setLoading(false);
490
+ }, []);
491
+ react.useEffect(() => {
492
+ loadLogs();
493
+ }, [loadLogs]);
494
+ const statusColor = (status) => {
495
+ switch (status) {
496
+ case "sent":
497
+ return "green";
498
+ case "delivered":
499
+ return "green";
500
+ case "failed":
501
+ return "red";
502
+ default:
503
+ return "grey";
504
+ }
505
+ };
506
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-6", children: [
507
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-4", children: [
508
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Message Logs" }),
509
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: loadLogs, children: "Refresh" })
510
+ ] }),
511
+ loading ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading logs..." }) : logs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted", children: "No messages sent yet." }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
512
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
513
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Event" }),
514
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Template" }),
515
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Recipient" }),
516
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
517
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Time" })
518
+ ] }) }),
519
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: logs.map((log) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
520
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: "blue", children: log.event_name }) }),
521
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: log.template_name }),
522
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: log.recipient_phone }),
523
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: statusColor(log.status), children: log.status }) }),
524
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: new Date(log.created_at).toLocaleString() }) })
525
+ ] }, log.id)) })
526
+ ] })
527
+ ] });
528
+ }
529
+ const WhatsAppPage = () => {
530
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4", children: [
531
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Toaster, {}),
532
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
533
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ChatBubbleLeftRight, {}),
534
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "WhatsApp Business" })
535
+ ] }),
536
+ /* @__PURE__ */ jsxRuntime.jsx(ConfigSection, {}),
537
+ /* @__PURE__ */ jsxRuntime.jsx(MappingsSection, {}),
538
+ /* @__PURE__ */ jsxRuntime.jsx(TestSection, {}),
539
+ /* @__PURE__ */ jsxRuntime.jsx(LogsSection, {})
540
+ ] });
541
+ };
542
+ const config = adminSdk.defineRouteConfig({
543
+ label: "WhatsApp",
544
+ icon: icons.ChatBubbleLeftRight
545
+ });
546
+ const i18nTranslations0 = {};
547
+ const widgetModule = { widgets: [] };
548
+ const routeModule = {
549
+ routes: [
550
+ {
551
+ Component: WhatsAppPage,
552
+ path: "/whatsapp"
553
+ }
554
+ ]
555
+ };
556
+ const menuItemModule = {
557
+ menuItems: [
558
+ {
559
+ label: config.label,
560
+ icon: config.icon,
561
+ path: "/whatsapp",
562
+ nested: void 0,
563
+ rank: void 0,
564
+ translationNs: void 0
565
+ }
566
+ ]
567
+ };
568
+ const formModule = { customFields: {} };
569
+ const displayModule = {
570
+ displays: {}
571
+ };
572
+ const i18nModule = { resources: i18nTranslations0 };
573
+ const plugin = {
574
+ widgetModule,
575
+ routeModule,
576
+ menuItemModule,
577
+ formModule,
578
+ displayModule,
579
+ i18nModule
580
+ };
581
+ module.exports = plugin;