medusa-contact-us 0.0.20 → 0.0.21

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.
@@ -4,7 +4,8 @@ const react = require("react");
4
4
  const adminSdk = require("@medusajs/admin-sdk");
5
5
  const ui = require("@medusajs/ui");
6
6
  const icons = require("@medusajs/icons");
7
- const useDebounce = (value, delay) => {
7
+ const reactRouterDom = require("react-router-dom");
8
+ const useDebounce$1 = (value, delay) => {
8
9
  const [debouncedValue, setDebouncedValue] = react.useState(value);
9
10
  react.useEffect(() => {
10
11
  const handler = setTimeout(() => setDebouncedValue(value), delay);
@@ -27,7 +28,7 @@ const ContactEmailSubscriptionsPage = () => {
27
28
  const [items, setItems] = react.useState([]);
28
29
  const [statusFilter, setStatusFilter] = react.useState("all");
29
30
  const [query, setQuery] = react.useState("");
30
- const debouncedQuery = useDebounce(query, 300);
31
+ const debouncedQuery = useDebounce$1(query, 300);
31
32
  const [isLoading, setIsLoading] = react.useState(true);
32
33
  const [isFetchingMore, setIsFetchingMore] = react.useState(false);
33
34
  const [error, setError] = react.useState(null);
@@ -165,10 +166,478 @@ const ContactEmailSubscriptionsPage = () => {
165
166
  ) }) : null
166
167
  ] }) });
167
168
  };
168
- const config = adminSdk.defineRouteConfig({
169
+ const config$2 = adminSdk.defineRouteConfig({
169
170
  label: "Contact email list",
170
171
  icon: icons.Envelope
171
172
  });
173
+ const useDebounce = (value, delay) => {
174
+ const [debouncedValue, setDebouncedValue] = react.useState(value);
175
+ react.useEffect(() => {
176
+ const handler = setTimeout(() => setDebouncedValue(value), delay);
177
+ return () => clearTimeout(handler);
178
+ }, [value, delay]);
179
+ return debouncedValue;
180
+ };
181
+ const getStatusBadgeClass$1 = (status) => {
182
+ const statusLower = status.toLowerCase();
183
+ if (statusLower === "pending") {
184
+ return "bg-ui-tag-orange-bg text-ui-tag-orange-text";
185
+ }
186
+ if (statusLower === "in_progress") {
187
+ return "bg-ui-tag-blue-bg text-ui-tag-blue-text";
188
+ }
189
+ if (statusLower === "resolved") {
190
+ return "bg-ui-tag-green-bg text-ui-tag-green-text";
191
+ }
192
+ if (statusLower === "closed") {
193
+ return "bg-ui-tag-grey-bg text-ui-tag-grey-text";
194
+ }
195
+ return "bg-ui-tag-purple-bg text-ui-tag-purple-text";
196
+ };
197
+ const ContactRequestsPage = () => {
198
+ const navigate = reactRouterDom.useNavigate();
199
+ const [items, setItems] = react.useState([]);
200
+ const [statusFilter, setStatusFilter] = react.useState("all");
201
+ const [emailQuery, setEmailQuery] = react.useState("");
202
+ const [sourceFilter, setSourceFilter] = react.useState("all");
203
+ const debouncedEmailQuery = useDebounce(emailQuery, 300);
204
+ const [isLoading, setIsLoading] = react.useState(true);
205
+ const [isFetchingMore, setIsFetchingMore] = react.useState(false);
206
+ const [error, setError] = react.useState(null);
207
+ const [offset, setOffset] = react.useState(0);
208
+ const [count, setCount] = react.useState(0);
209
+ const limit = 50;
210
+ const loadRequests = react.useCallback(
211
+ async (nextOffset, replace = false) => {
212
+ var _a;
213
+ try {
214
+ if (replace) {
215
+ setIsLoading(true);
216
+ } else {
217
+ setIsFetchingMore(true);
218
+ }
219
+ setError(null);
220
+ const params = new URLSearchParams();
221
+ params.set("limit", String(limit));
222
+ params.set("offset", String(nextOffset));
223
+ if (statusFilter !== "all") {
224
+ params.set("status", statusFilter);
225
+ }
226
+ if (debouncedEmailQuery.trim()) {
227
+ params.set("email", debouncedEmailQuery.trim());
228
+ }
229
+ if (sourceFilter !== "all") {
230
+ params.set("source", sourceFilter);
231
+ }
232
+ params.set("order", "created_at");
233
+ params.set("order_direction", "DESC");
234
+ const response = await fetch(
235
+ `/admin/contact-requests?${params.toString()}`,
236
+ { credentials: "include" }
237
+ );
238
+ if (!response.ok) {
239
+ const message = await response.text();
240
+ throw new Error(message || "Unable to load contact requests");
241
+ }
242
+ const payload = await response.json();
243
+ setCount(payload.count ?? 0);
244
+ setOffset(nextOffset + (((_a = payload.requests) == null ? void 0 : _a.length) ?? 0));
245
+ setItems(
246
+ (prev) => replace ? payload.requests ?? [] : [...prev, ...payload.requests ?? []]
247
+ );
248
+ } catch (loadError) {
249
+ const message = loadError instanceof Error ? loadError.message : "Unable to load contact requests";
250
+ setError(message);
251
+ } finally {
252
+ setIsLoading(false);
253
+ setIsFetchingMore(false);
254
+ }
255
+ },
256
+ [statusFilter, debouncedEmailQuery, sourceFilter]
257
+ );
258
+ react.useEffect(() => {
259
+ void loadRequests(0, true);
260
+ }, [statusFilter, debouncedEmailQuery, sourceFilter, loadRequests]);
261
+ const hasMore = react.useMemo(() => offset < count, [offset, count]);
262
+ const availableStatuses = react.useMemo(() => {
263
+ const statuses = /* @__PURE__ */ new Set();
264
+ items.forEach((item) => statuses.add(item.status));
265
+ return Array.from(statuses).sort();
266
+ }, [items]);
267
+ const availableSources = react.useMemo(() => {
268
+ const sources = /* @__PURE__ */ new Set();
269
+ items.forEach((item) => {
270
+ if (item.source) {
271
+ sources.add(item.source);
272
+ }
273
+ });
274
+ return Array.from(sources).sort();
275
+ }, [items]);
276
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "mx-auto flex w-full max-w-7xl flex-col gap-6 p-6", children: [
277
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
278
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
279
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Contact Requests" }),
280
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Manage and track customer contact requests from the storefront" })
281
+ ] }),
282
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "primary", onClick: () => loadRequests(0, true), children: "Refresh" })
283
+ ] }),
284
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
285
+ /* @__PURE__ */ jsxRuntime.jsx(
286
+ ui.Input,
287
+ {
288
+ placeholder: "Search by email",
289
+ value: emailQuery,
290
+ onChange: (event) => setEmailQuery(event.target.value),
291
+ className: "md:max-w-sm"
292
+ }
293
+ ),
294
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-3", children: [
295
+ /* @__PURE__ */ jsxRuntime.jsxs(
296
+ "select",
297
+ {
298
+ value: statusFilter,
299
+ onChange: (event) => setStatusFilter(event.target.value),
300
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive md:max-w-xs",
301
+ children: [
302
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All Statuses" }),
303
+ availableStatuses.map((status) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: status, children: status.replace("_", " ").toUpperCase() }, status))
304
+ ]
305
+ }
306
+ ),
307
+ /* @__PURE__ */ jsxRuntime.jsxs(
308
+ "select",
309
+ {
310
+ value: sourceFilter,
311
+ onChange: (event) => setSourceFilter(event.target.value),
312
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive md:max-w-xs",
313
+ children: [
314
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All Sources" }),
315
+ availableSources.map((source) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: source, children: source }, source))
316
+ ]
317
+ }
318
+ )
319
+ ] })
320
+ ] }),
321
+ error ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-strong p-6 text-center", children: [
322
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "plus", className: "text-ui-fg-error", children: error }),
323
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4 flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
324
+ ui.Button,
325
+ {
326
+ variant: "secondary",
327
+ onClick: () => loadRequests(0, true),
328
+ children: "Try again"
329
+ }
330
+ ) })
331
+ ] }) : null,
332
+ isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center py-16", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading contact requests..." }) }) : items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-dashed border-ui-border-strong p-10 text-center", children: [
333
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", className: "text-xl", children: "No contact requests yet" }),
334
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "mt-2 text-ui-fg-subtle", children: "Contact requests created through the storefront will appear here." })
335
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-xl border border-ui-border-base", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
336
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
337
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Email" }),
338
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Status" }),
339
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Source" }),
340
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Created" }),
341
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Actions" })
342
+ ] }) }),
343
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "divide-y divide-ui-border-subtle", children: items.map((request) => /* @__PURE__ */ jsxRuntime.jsxs(
344
+ "tr",
345
+ {
346
+ className: "hover:bg-ui-bg-subtle/60 cursor-pointer",
347
+ onClick: () => navigate(`/contact-requests/${request.id}`),
348
+ children: [
349
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4 font-medium text-ui-fg-base", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-0.5", children: [
350
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: request.email }),
351
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: request.id })
352
+ ] }) }),
353
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(
354
+ ui.Badge,
355
+ {
356
+ size: "2xsmall",
357
+ className: `uppercase ${getStatusBadgeClass$1(request.status)}`,
358
+ children: request.status.replace("_", " ")
359
+ }
360
+ ) }),
361
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: request.source ?? "storefront" }),
362
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: new Date(request.created_at).toLocaleString() }),
363
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(
364
+ ui.Button,
365
+ {
366
+ variant: "transparent",
367
+ size: "small",
368
+ onClick: (e) => {
369
+ e.stopPropagation();
370
+ navigate(`/contact-requests/${request.id}`);
371
+ },
372
+ children: "View"
373
+ }
374
+ ) })
375
+ ]
376
+ },
377
+ request.id
378
+ )) })
379
+ ] }) }),
380
+ hasMore ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
381
+ ui.Button,
382
+ {
383
+ variant: "secondary",
384
+ isLoading: isFetchingMore,
385
+ onClick: () => loadRequests(offset, false),
386
+ children: "Load more"
387
+ }
388
+ ) }) : null
389
+ ] }) });
390
+ };
391
+ const config$1 = adminSdk.defineRouteConfig({
392
+ label: "Contact Requests",
393
+ icon: icons.ChatBubbleLeftRight
394
+ });
395
+ const getStatusBadgeClass = (status) => {
396
+ const statusLower = status.toLowerCase();
397
+ if (statusLower === "pending") {
398
+ return "bg-ui-tag-orange-bg text-ui-tag-orange-text";
399
+ }
400
+ if (statusLower === "in_progress") {
401
+ return "bg-ui-tag-blue-bg text-ui-tag-blue-text";
402
+ }
403
+ if (statusLower === "resolved") {
404
+ return "bg-ui-tag-green-bg text-ui-tag-green-text";
405
+ }
406
+ if (statusLower === "closed") {
407
+ return "bg-ui-tag-grey-bg text-ui-tag-grey-text";
408
+ }
409
+ return "bg-ui-tag-purple-bg text-ui-tag-purple-text";
410
+ };
411
+ const ContactRequestDetailPage = () => {
412
+ const navigate = reactRouterDom.useNavigate();
413
+ const { id } = reactRouterDom.useParams();
414
+ const [request, setRequest] = react.useState(null);
415
+ const [nextAllowedStatuses, setNextAllowedStatuses] = react.useState([]);
416
+ const [selectedStatus, setSelectedStatus] = react.useState("");
417
+ const [isLoading, setIsLoading] = react.useState(true);
418
+ const [isUpdating, setIsUpdating] = react.useState(false);
419
+ const [error, setError] = react.useState(null);
420
+ const [updateError, setUpdateError] = react.useState(null);
421
+ const [updateSuccess, setUpdateSuccess] = react.useState(false);
422
+ react.useEffect(() => {
423
+ if (!id) {
424
+ navigate("/contact-requests");
425
+ return;
426
+ }
427
+ const loadRequest = async () => {
428
+ try {
429
+ setIsLoading(true);
430
+ setError(null);
431
+ const response = await fetch(`/admin/contact-requests/${id}`, {
432
+ credentials: "include"
433
+ });
434
+ if (!response.ok) {
435
+ const message = await response.text();
436
+ throw new Error(message || "Unable to load contact request");
437
+ }
438
+ const payload = await response.json();
439
+ setRequest(payload.request);
440
+ setNextAllowedStatuses(payload.next_allowed_statuses ?? []);
441
+ setSelectedStatus("");
442
+ } catch (loadError) {
443
+ const message = loadError instanceof Error ? loadError.message : "Unable to load contact request";
444
+ setError(message);
445
+ } finally {
446
+ setIsLoading(false);
447
+ }
448
+ };
449
+ void loadRequest();
450
+ }, [id, navigate]);
451
+ const handleStatusUpdate = async () => {
452
+ if (!id || !selectedStatus) {
453
+ return;
454
+ }
455
+ try {
456
+ setIsUpdating(true);
457
+ setUpdateError(null);
458
+ setUpdateSuccess(false);
459
+ const response = await fetch(`/admin/contact-requests/${id}/status`, {
460
+ method: "POST",
461
+ headers: {
462
+ "Content-Type": "application/json"
463
+ },
464
+ credentials: "include",
465
+ body: JSON.stringify({ status: selectedStatus })
466
+ });
467
+ if (!response.ok) {
468
+ const message = await response.text();
469
+ throw new Error(message || "Unable to update status");
470
+ }
471
+ const payload = await response.json();
472
+ setRequest(payload.request);
473
+ setSelectedStatus("");
474
+ setUpdateSuccess(true);
475
+ setTimeout(() => setUpdateSuccess(false), 3e3);
476
+ const detailResponse = await fetch(`/admin/contact-requests/${id}`, {
477
+ credentials: "include"
478
+ });
479
+ if (detailResponse.ok) {
480
+ const detailPayload = await detailResponse.json();
481
+ setNextAllowedStatuses(detailPayload.next_allowed_statuses ?? []);
482
+ }
483
+ } catch (updateErr) {
484
+ const message = updateErr instanceof Error ? updateErr.message : "Unable to update status";
485
+ setUpdateError(message);
486
+ } finally {
487
+ setIsUpdating(false);
488
+ }
489
+ };
490
+ if (isLoading) {
491
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center py-16", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading contact request..." }) }) }) });
492
+ }
493
+ if (error || !request) {
494
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-strong p-6 text-center", children: [
495
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "plus", className: "text-ui-fg-error", children: error || "Contact request not found" }),
496
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4 flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => navigate("/contact-requests"), children: "Back to list" }) })
497
+ ] }) }) });
498
+ }
499
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: [
500
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-3", children: [
501
+ /* @__PURE__ */ jsxRuntime.jsxs(
502
+ ui.Button,
503
+ {
504
+ variant: "transparent",
505
+ size: "small",
506
+ onClick: () => navigate("/contact-requests"),
507
+ className: "w-fit",
508
+ children: [
509
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowLeft, { className: "mr-2" }),
510
+ "Back to list"
511
+ ]
512
+ }
513
+ ),
514
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1 md:flex-row md:items-center md:justify-between", children: [
515
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
516
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Contact Request Details" }),
517
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: request.id })
518
+ ] }),
519
+ /* @__PURE__ */ jsxRuntime.jsx(
520
+ ui.Badge,
521
+ {
522
+ size: "small",
523
+ className: `uppercase ${getStatusBadgeClass(request.status)}`,
524
+ children: request.status.replace("_", " ")
525
+ }
526
+ )
527
+ ] })
528
+ ] }),
529
+ nextAllowedStatuses.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
530
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Update Status" }),
531
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-end", children: [
532
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
533
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "mb-2 block text-sm font-medium text-ui-fg-base", children: "New Status" }),
534
+ /* @__PURE__ */ jsxRuntime.jsxs(
535
+ "select",
536
+ {
537
+ value: selectedStatus,
538
+ onChange: (event) => setSelectedStatus(event.target.value),
539
+ className: "h-9 w-full rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive",
540
+ children: [
541
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select new status" }),
542
+ nextAllowedStatuses.map((status) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: status, children: status.replace("_", " ").toUpperCase() }, status))
543
+ ]
544
+ }
545
+ )
546
+ ] }),
547
+ /* @__PURE__ */ jsxRuntime.jsx(
548
+ ui.Button,
549
+ {
550
+ variant: "primary",
551
+ onClick: handleStatusUpdate,
552
+ disabled: !selectedStatus || isUpdating,
553
+ isLoading: isUpdating,
554
+ children: "Update Status"
555
+ }
556
+ )
557
+ ] }),
558
+ updateError && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "mt-2 text-ui-fg-error", children: updateError }),
559
+ updateSuccess && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "mt-2 text-ui-fg-success", children: "Status updated successfully" })
560
+ ] }),
561
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-6 md:grid-cols-2", children: [
562
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
563
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Contact Information" }),
564
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
565
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
566
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Email" }),
567
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: request.email })
568
+ ] }),
569
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
570
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Source" }),
571
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: request.source ?? "storefront" })
572
+ ] }),
573
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
574
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Created" }),
575
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: new Date(request.created_at).toLocaleString() })
576
+ ] }),
577
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
578
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Last Updated" }),
579
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: new Date(request.updated_at).toLocaleString() })
580
+ ] })
581
+ ] })
582
+ ] }),
583
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
584
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Status History" }),
585
+ request.status_history && request.status_history.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: request.status_history.map((entry, index) => /* @__PURE__ */ jsxRuntime.jsx(
586
+ "div",
587
+ {
588
+ className: "flex items-center justify-between border-b border-ui-border-subtle pb-2 last:border-0",
589
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
590
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
591
+ /* @__PURE__ */ jsxRuntime.jsx(
592
+ ui.Badge,
593
+ {
594
+ size: "2xsmall",
595
+ className: `uppercase ${getStatusBadgeClass(entry.to)}`,
596
+ children: entry.to.replace("_", " ")
597
+ }
598
+ ),
599
+ entry.from && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
600
+ "from ",
601
+ entry.from.replace("_", " ")
602
+ ] })
603
+ ] }),
604
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
605
+ new Date(entry.changed_at).toLocaleString(),
606
+ entry.changed_by && ` by ${entry.changed_by}`
607
+ ] })
608
+ ] })
609
+ },
610
+ index
611
+ )) }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "No status history available" })
612
+ ] })
613
+ ] }),
614
+ request.payload && Object.keys(request.payload).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
615
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Request Details" }),
616
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-3", children: Object.entries(request.payload).map(([key, value]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
617
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: key.charAt(0).toUpperCase() + key.slice(1).replace("_", " ") }),
618
+ /* @__PURE__ */ jsxRuntime.jsx(
619
+ ui.Textarea,
620
+ {
621
+ value: typeof value === "string" ? value : JSON.stringify(value, null, 2),
622
+ readOnly: true,
623
+ className: "mt-1 min-h-[60px]"
624
+ }
625
+ )
626
+ ] }, key)) })
627
+ ] }),
628
+ request.metadata && Object.keys(request.metadata).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
629
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Metadata" }),
630
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: Object.entries(request.metadata).map(([key, value]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between", children: [
631
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: key }),
632
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "font-medium", children: typeof value === "string" ? value : JSON.stringify(value) })
633
+ ] }, key)) })
634
+ ] })
635
+ ] }) });
636
+ };
637
+ const config = adminSdk.defineRouteConfig({
638
+ label: "Contact Request Details",
639
+ icon: icons.ChatBubbleLeftRight
640
+ });
172
641
  const en = {};
173
642
  const i18nTranslations0 = {
174
643
  en
@@ -179,15 +648,35 @@ const routeModule = {
179
648
  {
180
649
  Component: ContactEmailSubscriptionsPage,
181
650
  path: "/contact-email-subscriptions"
651
+ },
652
+ {
653
+ Component: ContactRequestsPage,
654
+ path: "/contact-requests"
655
+ },
656
+ {
657
+ Component: ContactRequestDetailPage,
658
+ path: "/contact-requests/:id"
182
659
  }
183
660
  ]
184
661
  };
185
662
  const menuItemModule = {
186
663
  menuItems: [
664
+ {
665
+ label: config$2.label,
666
+ icon: config$2.icon,
667
+ path: "/contact-email-subscriptions",
668
+ nested: void 0
669
+ },
670
+ {
671
+ label: config$1.label,
672
+ icon: config$1.icon,
673
+ path: "/contact-requests",
674
+ nested: void 0
675
+ },
187
676
  {
188
677
  label: config.label,
189
678
  icon: config.icon,
190
- path: "/contact-email-subscriptions",
679
+ path: "/contact-requests/:id",
191
680
  nested: void 0
192
681
  }
193
682
  ]
@@ -1,9 +1,10 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useState, useCallback, useEffect, useMemo } from "react";
3
3
  import { defineRouteConfig } from "@medusajs/admin-sdk";
4
- import { Container, Heading, Text, Button, Input, Badge } from "@medusajs/ui";
5
- import { Envelope } from "@medusajs/icons";
6
- const useDebounce = (value, delay) => {
4
+ import { Container, Heading, Text, Button, Input, Badge, Textarea } from "@medusajs/ui";
5
+ import { Envelope, ChatBubbleLeftRight, ArrowLeft } from "@medusajs/icons";
6
+ import { useNavigate, useParams } from "react-router-dom";
7
+ const useDebounce$1 = (value, delay) => {
7
8
  const [debouncedValue, setDebouncedValue] = useState(value);
8
9
  useEffect(() => {
9
10
  const handler = setTimeout(() => setDebouncedValue(value), delay);
@@ -26,7 +27,7 @@ const ContactEmailSubscriptionsPage = () => {
26
27
  const [items, setItems] = useState([]);
27
28
  const [statusFilter, setStatusFilter] = useState("all");
28
29
  const [query, setQuery] = useState("");
29
- const debouncedQuery = useDebounce(query, 300);
30
+ const debouncedQuery = useDebounce$1(query, 300);
30
31
  const [isLoading, setIsLoading] = useState(true);
31
32
  const [isFetchingMore, setIsFetchingMore] = useState(false);
32
33
  const [error, setError] = useState(null);
@@ -164,10 +165,478 @@ const ContactEmailSubscriptionsPage = () => {
164
165
  ) }) : null
165
166
  ] }) });
166
167
  };
167
- const config = defineRouteConfig({
168
+ const config$2 = defineRouteConfig({
168
169
  label: "Contact email list",
169
170
  icon: Envelope
170
171
  });
172
+ const useDebounce = (value, delay) => {
173
+ const [debouncedValue, setDebouncedValue] = useState(value);
174
+ useEffect(() => {
175
+ const handler = setTimeout(() => setDebouncedValue(value), delay);
176
+ return () => clearTimeout(handler);
177
+ }, [value, delay]);
178
+ return debouncedValue;
179
+ };
180
+ const getStatusBadgeClass$1 = (status) => {
181
+ const statusLower = status.toLowerCase();
182
+ if (statusLower === "pending") {
183
+ return "bg-ui-tag-orange-bg text-ui-tag-orange-text";
184
+ }
185
+ if (statusLower === "in_progress") {
186
+ return "bg-ui-tag-blue-bg text-ui-tag-blue-text";
187
+ }
188
+ if (statusLower === "resolved") {
189
+ return "bg-ui-tag-green-bg text-ui-tag-green-text";
190
+ }
191
+ if (statusLower === "closed") {
192
+ return "bg-ui-tag-grey-bg text-ui-tag-grey-text";
193
+ }
194
+ return "bg-ui-tag-purple-bg text-ui-tag-purple-text";
195
+ };
196
+ const ContactRequestsPage = () => {
197
+ const navigate = useNavigate();
198
+ const [items, setItems] = useState([]);
199
+ const [statusFilter, setStatusFilter] = useState("all");
200
+ const [emailQuery, setEmailQuery] = useState("");
201
+ const [sourceFilter, setSourceFilter] = useState("all");
202
+ const debouncedEmailQuery = useDebounce(emailQuery, 300);
203
+ const [isLoading, setIsLoading] = useState(true);
204
+ const [isFetchingMore, setIsFetchingMore] = useState(false);
205
+ const [error, setError] = useState(null);
206
+ const [offset, setOffset] = useState(0);
207
+ const [count, setCount] = useState(0);
208
+ const limit = 50;
209
+ const loadRequests = useCallback(
210
+ async (nextOffset, replace = false) => {
211
+ var _a;
212
+ try {
213
+ if (replace) {
214
+ setIsLoading(true);
215
+ } else {
216
+ setIsFetchingMore(true);
217
+ }
218
+ setError(null);
219
+ const params = new URLSearchParams();
220
+ params.set("limit", String(limit));
221
+ params.set("offset", String(nextOffset));
222
+ if (statusFilter !== "all") {
223
+ params.set("status", statusFilter);
224
+ }
225
+ if (debouncedEmailQuery.trim()) {
226
+ params.set("email", debouncedEmailQuery.trim());
227
+ }
228
+ if (sourceFilter !== "all") {
229
+ params.set("source", sourceFilter);
230
+ }
231
+ params.set("order", "created_at");
232
+ params.set("order_direction", "DESC");
233
+ const response = await fetch(
234
+ `/admin/contact-requests?${params.toString()}`,
235
+ { credentials: "include" }
236
+ );
237
+ if (!response.ok) {
238
+ const message = await response.text();
239
+ throw new Error(message || "Unable to load contact requests");
240
+ }
241
+ const payload = await response.json();
242
+ setCount(payload.count ?? 0);
243
+ setOffset(nextOffset + (((_a = payload.requests) == null ? void 0 : _a.length) ?? 0));
244
+ setItems(
245
+ (prev) => replace ? payload.requests ?? [] : [...prev, ...payload.requests ?? []]
246
+ );
247
+ } catch (loadError) {
248
+ const message = loadError instanceof Error ? loadError.message : "Unable to load contact requests";
249
+ setError(message);
250
+ } finally {
251
+ setIsLoading(false);
252
+ setIsFetchingMore(false);
253
+ }
254
+ },
255
+ [statusFilter, debouncedEmailQuery, sourceFilter]
256
+ );
257
+ useEffect(() => {
258
+ void loadRequests(0, true);
259
+ }, [statusFilter, debouncedEmailQuery, sourceFilter, loadRequests]);
260
+ const hasMore = useMemo(() => offset < count, [offset, count]);
261
+ const availableStatuses = useMemo(() => {
262
+ const statuses = /* @__PURE__ */ new Set();
263
+ items.forEach((item) => statuses.add(item.status));
264
+ return Array.from(statuses).sort();
265
+ }, [items]);
266
+ const availableSources = useMemo(() => {
267
+ const sources = /* @__PURE__ */ new Set();
268
+ items.forEach((item) => {
269
+ if (item.source) {
270
+ sources.add(item.source);
271
+ }
272
+ });
273
+ return Array.from(sources).sort();
274
+ }, [items]);
275
+ return /* @__PURE__ */ jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxs(Container, { className: "mx-auto flex w-full max-w-7xl flex-col gap-6 p-6", children: [
276
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
277
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
278
+ /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Contact Requests" }),
279
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Manage and track customer contact requests from the storefront" })
280
+ ] }),
281
+ /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: () => loadRequests(0, true), children: "Refresh" })
282
+ ] }),
283
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
284
+ /* @__PURE__ */ jsx(
285
+ Input,
286
+ {
287
+ placeholder: "Search by email",
288
+ value: emailQuery,
289
+ onChange: (event) => setEmailQuery(event.target.value),
290
+ className: "md:max-w-sm"
291
+ }
292
+ ),
293
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [
294
+ /* @__PURE__ */ jsxs(
295
+ "select",
296
+ {
297
+ value: statusFilter,
298
+ onChange: (event) => setStatusFilter(event.target.value),
299
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive md:max-w-xs",
300
+ children: [
301
+ /* @__PURE__ */ jsx("option", { value: "all", children: "All Statuses" }),
302
+ availableStatuses.map((status) => /* @__PURE__ */ jsx("option", { value: status, children: status.replace("_", " ").toUpperCase() }, status))
303
+ ]
304
+ }
305
+ ),
306
+ /* @__PURE__ */ jsxs(
307
+ "select",
308
+ {
309
+ value: sourceFilter,
310
+ onChange: (event) => setSourceFilter(event.target.value),
311
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive md:max-w-xs",
312
+ children: [
313
+ /* @__PURE__ */ jsx("option", { value: "all", children: "All Sources" }),
314
+ availableSources.map((source) => /* @__PURE__ */ jsx("option", { value: source, children: source }, source))
315
+ ]
316
+ }
317
+ )
318
+ ] })
319
+ ] }),
320
+ error ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-strong p-6 text-center", children: [
321
+ /* @__PURE__ */ jsx(Text, { weight: "plus", className: "text-ui-fg-error", children: error }),
322
+ /* @__PURE__ */ jsx("div", { className: "mt-4 flex justify-center", children: /* @__PURE__ */ jsx(
323
+ Button,
324
+ {
325
+ variant: "secondary",
326
+ onClick: () => loadRequests(0, true),
327
+ children: "Try again"
328
+ }
329
+ ) })
330
+ ] }) : null,
331
+ isLoading ? /* @__PURE__ */ jsx("div", { className: "flex justify-center py-16", children: /* @__PURE__ */ jsx(Text, { children: "Loading contact requests..." }) }) : items.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-dashed border-ui-border-strong p-10 text-center", children: [
332
+ /* @__PURE__ */ jsx(Heading, { level: "h3", className: "text-xl", children: "No contact requests yet" }),
333
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "mt-2 text-ui-fg-subtle", children: "Contact requests created through the storefront will appear here." })
334
+ ] }) : /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-xl border border-ui-border-base", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
335
+ /* @__PURE__ */ jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxs("tr", { children: [
336
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Email" }),
337
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Status" }),
338
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Source" }),
339
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Created" }),
340
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Actions" })
341
+ ] }) }),
342
+ /* @__PURE__ */ jsx("tbody", { className: "divide-y divide-ui-border-subtle", children: items.map((request) => /* @__PURE__ */ jsxs(
343
+ "tr",
344
+ {
345
+ className: "hover:bg-ui-bg-subtle/60 cursor-pointer",
346
+ onClick: () => navigate(`/contact-requests/${request.id}`),
347
+ children: [
348
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-4 font-medium text-ui-fg-base", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
349
+ /* @__PURE__ */ jsx("span", { children: request.email }),
350
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: request.id })
351
+ ] }) }),
352
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-4", children: /* @__PURE__ */ jsx(
353
+ Badge,
354
+ {
355
+ size: "2xsmall",
356
+ className: `uppercase ${getStatusBadgeClass$1(request.status)}`,
357
+ children: request.status.replace("_", " ")
358
+ }
359
+ ) }),
360
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: request.source ?? "storefront" }),
361
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: new Date(request.created_at).toLocaleString() }),
362
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-4", children: /* @__PURE__ */ jsx(
363
+ Button,
364
+ {
365
+ variant: "transparent",
366
+ size: "small",
367
+ onClick: (e) => {
368
+ e.stopPropagation();
369
+ navigate(`/contact-requests/${request.id}`);
370
+ },
371
+ children: "View"
372
+ }
373
+ ) })
374
+ ]
375
+ },
376
+ request.id
377
+ )) })
378
+ ] }) }),
379
+ hasMore ? /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(
380
+ Button,
381
+ {
382
+ variant: "secondary",
383
+ isLoading: isFetchingMore,
384
+ onClick: () => loadRequests(offset, false),
385
+ children: "Load more"
386
+ }
387
+ ) }) : null
388
+ ] }) });
389
+ };
390
+ const config$1 = defineRouteConfig({
391
+ label: "Contact Requests",
392
+ icon: ChatBubbleLeftRight
393
+ });
394
+ const getStatusBadgeClass = (status) => {
395
+ const statusLower = status.toLowerCase();
396
+ if (statusLower === "pending") {
397
+ return "bg-ui-tag-orange-bg text-ui-tag-orange-text";
398
+ }
399
+ if (statusLower === "in_progress") {
400
+ return "bg-ui-tag-blue-bg text-ui-tag-blue-text";
401
+ }
402
+ if (statusLower === "resolved") {
403
+ return "bg-ui-tag-green-bg text-ui-tag-green-text";
404
+ }
405
+ if (statusLower === "closed") {
406
+ return "bg-ui-tag-grey-bg text-ui-tag-grey-text";
407
+ }
408
+ return "bg-ui-tag-purple-bg text-ui-tag-purple-text";
409
+ };
410
+ const ContactRequestDetailPage = () => {
411
+ const navigate = useNavigate();
412
+ const { id } = useParams();
413
+ const [request, setRequest] = useState(null);
414
+ const [nextAllowedStatuses, setNextAllowedStatuses] = useState([]);
415
+ const [selectedStatus, setSelectedStatus] = useState("");
416
+ const [isLoading, setIsLoading] = useState(true);
417
+ const [isUpdating, setIsUpdating] = useState(false);
418
+ const [error, setError] = useState(null);
419
+ const [updateError, setUpdateError] = useState(null);
420
+ const [updateSuccess, setUpdateSuccess] = useState(false);
421
+ useEffect(() => {
422
+ if (!id) {
423
+ navigate("/contact-requests");
424
+ return;
425
+ }
426
+ const loadRequest = async () => {
427
+ try {
428
+ setIsLoading(true);
429
+ setError(null);
430
+ const response = await fetch(`/admin/contact-requests/${id}`, {
431
+ credentials: "include"
432
+ });
433
+ if (!response.ok) {
434
+ const message = await response.text();
435
+ throw new Error(message || "Unable to load contact request");
436
+ }
437
+ const payload = await response.json();
438
+ setRequest(payload.request);
439
+ setNextAllowedStatuses(payload.next_allowed_statuses ?? []);
440
+ setSelectedStatus("");
441
+ } catch (loadError) {
442
+ const message = loadError instanceof Error ? loadError.message : "Unable to load contact request";
443
+ setError(message);
444
+ } finally {
445
+ setIsLoading(false);
446
+ }
447
+ };
448
+ void loadRequest();
449
+ }, [id, navigate]);
450
+ const handleStatusUpdate = async () => {
451
+ if (!id || !selectedStatus) {
452
+ return;
453
+ }
454
+ try {
455
+ setIsUpdating(true);
456
+ setUpdateError(null);
457
+ setUpdateSuccess(false);
458
+ const response = await fetch(`/admin/contact-requests/${id}/status`, {
459
+ method: "POST",
460
+ headers: {
461
+ "Content-Type": "application/json"
462
+ },
463
+ credentials: "include",
464
+ body: JSON.stringify({ status: selectedStatus })
465
+ });
466
+ if (!response.ok) {
467
+ const message = await response.text();
468
+ throw new Error(message || "Unable to update status");
469
+ }
470
+ const payload = await response.json();
471
+ setRequest(payload.request);
472
+ setSelectedStatus("");
473
+ setUpdateSuccess(true);
474
+ setTimeout(() => setUpdateSuccess(false), 3e3);
475
+ const detailResponse = await fetch(`/admin/contact-requests/${id}`, {
476
+ credentials: "include"
477
+ });
478
+ if (detailResponse.ok) {
479
+ const detailPayload = await detailResponse.json();
480
+ setNextAllowedStatuses(detailPayload.next_allowed_statuses ?? []);
481
+ }
482
+ } catch (updateErr) {
483
+ const message = updateErr instanceof Error ? updateErr.message : "Unable to update status";
484
+ setUpdateError(message);
485
+ } finally {
486
+ setIsUpdating(false);
487
+ }
488
+ };
489
+ if (isLoading) {
490
+ return /* @__PURE__ */ jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsx(Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: /* @__PURE__ */ jsx("div", { className: "flex justify-center py-16", children: /* @__PURE__ */ jsx(Text, { children: "Loading contact request..." }) }) }) });
491
+ }
492
+ if (error || !request) {
493
+ return /* @__PURE__ */ jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsx(Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-strong p-6 text-center", children: [
494
+ /* @__PURE__ */ jsx(Text, { weight: "plus", className: "text-ui-fg-error", children: error || "Contact request not found" }),
495
+ /* @__PURE__ */ jsx("div", { className: "mt-4 flex justify-center", children: /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => navigate("/contact-requests"), children: "Back to list" }) })
496
+ ] }) }) });
497
+ }
498
+ return /* @__PURE__ */ jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxs(Container, { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 p-6", children: [
499
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-3", children: [
500
+ /* @__PURE__ */ jsxs(
501
+ Button,
502
+ {
503
+ variant: "transparent",
504
+ size: "small",
505
+ onClick: () => navigate("/contact-requests"),
506
+ className: "w-fit",
507
+ children: [
508
+ /* @__PURE__ */ jsx(ArrowLeft, { className: "mr-2" }),
509
+ "Back to list"
510
+ ]
511
+ }
512
+ ),
513
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 md:flex-row md:items-center md:justify-between", children: [
514
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
515
+ /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Contact Request Details" }),
516
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: request.id })
517
+ ] }),
518
+ /* @__PURE__ */ jsx(
519
+ Badge,
520
+ {
521
+ size: "small",
522
+ className: `uppercase ${getStatusBadgeClass(request.status)}`,
523
+ children: request.status.replace("_", " ")
524
+ }
525
+ )
526
+ ] })
527
+ ] }),
528
+ nextAllowedStatuses.length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
529
+ /* @__PURE__ */ jsx(Heading, { level: "h2", className: "mb-4 text-lg", children: "Update Status" }),
530
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-end", children: [
531
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
532
+ /* @__PURE__ */ jsx("label", { className: "mb-2 block text-sm font-medium text-ui-fg-base", children: "New Status" }),
533
+ /* @__PURE__ */ jsxs(
534
+ "select",
535
+ {
536
+ value: selectedStatus,
537
+ onChange: (event) => setSelectedStatus(event.target.value),
538
+ className: "h-9 w-full rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive",
539
+ children: [
540
+ /* @__PURE__ */ jsx("option", { value: "", children: "Select new status" }),
541
+ nextAllowedStatuses.map((status) => /* @__PURE__ */ jsx("option", { value: status, children: status.replace("_", " ").toUpperCase() }, status))
542
+ ]
543
+ }
544
+ )
545
+ ] }),
546
+ /* @__PURE__ */ jsx(
547
+ Button,
548
+ {
549
+ variant: "primary",
550
+ onClick: handleStatusUpdate,
551
+ disabled: !selectedStatus || isUpdating,
552
+ isLoading: isUpdating,
553
+ children: "Update Status"
554
+ }
555
+ )
556
+ ] }),
557
+ updateError && /* @__PURE__ */ jsx(Text, { size: "small", className: "mt-2 text-ui-fg-error", children: updateError }),
558
+ updateSuccess && /* @__PURE__ */ jsx(Text, { size: "small", className: "mt-2 text-ui-fg-success", children: "Status updated successfully" })
559
+ ] }),
560
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-6 md:grid-cols-2", children: [
561
+ /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
562
+ /* @__PURE__ */ jsx(Heading, { level: "h2", className: "mb-4 text-lg", children: "Contact Information" }),
563
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
564
+ /* @__PURE__ */ jsxs("div", { children: [
565
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Email" }),
566
+ /* @__PURE__ */ jsx(Text, { className: "font-medium", children: request.email })
567
+ ] }),
568
+ /* @__PURE__ */ jsxs("div", { children: [
569
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Source" }),
570
+ /* @__PURE__ */ jsx(Text, { className: "font-medium", children: request.source ?? "storefront" })
571
+ ] }),
572
+ /* @__PURE__ */ jsxs("div", { children: [
573
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Created" }),
574
+ /* @__PURE__ */ jsx(Text, { className: "font-medium", children: new Date(request.created_at).toLocaleString() })
575
+ ] }),
576
+ /* @__PURE__ */ jsxs("div", { children: [
577
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Last Updated" }),
578
+ /* @__PURE__ */ jsx(Text, { className: "font-medium", children: new Date(request.updated_at).toLocaleString() })
579
+ ] })
580
+ ] })
581
+ ] }),
582
+ /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
583
+ /* @__PURE__ */ jsx(Heading, { level: "h2", className: "mb-4 text-lg", children: "Status History" }),
584
+ request.status_history && request.status_history.length > 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-2", children: request.status_history.map((entry, index) => /* @__PURE__ */ jsx(
585
+ "div",
586
+ {
587
+ className: "flex items-center justify-between border-b border-ui-border-subtle pb-2 last:border-0",
588
+ children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
589
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
590
+ /* @__PURE__ */ jsx(
591
+ Badge,
592
+ {
593
+ size: "2xsmall",
594
+ className: `uppercase ${getStatusBadgeClass(entry.to)}`,
595
+ children: entry.to.replace("_", " ")
596
+ }
597
+ ),
598
+ entry.from && /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
599
+ "from ",
600
+ entry.from.replace("_", " ")
601
+ ] })
602
+ ] }),
603
+ /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
604
+ new Date(entry.changed_at).toLocaleString(),
605
+ entry.changed_by && ` by ${entry.changed_by}`
606
+ ] })
607
+ ] })
608
+ },
609
+ index
610
+ )) }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "No status history available" })
611
+ ] })
612
+ ] }),
613
+ request.payload && Object.keys(request.payload).length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
614
+ /* @__PURE__ */ jsx(Heading, { level: "h2", className: "mb-4 text-lg", children: "Request Details" }),
615
+ /* @__PURE__ */ jsx("div", { className: "space-y-3", children: Object.entries(request.payload).map(([key, value]) => /* @__PURE__ */ jsxs("div", { children: [
616
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: key.charAt(0).toUpperCase() + key.slice(1).replace("_", " ") }),
617
+ /* @__PURE__ */ jsx(
618
+ Textarea,
619
+ {
620
+ value: typeof value === "string" ? value : JSON.stringify(value, null, 2),
621
+ readOnly: true,
622
+ className: "mt-1 min-h-[60px]"
623
+ }
624
+ )
625
+ ] }, key)) })
626
+ ] }),
627
+ request.metadata && Object.keys(request.metadata).length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
628
+ /* @__PURE__ */ jsx(Heading, { level: "h2", className: "mb-4 text-lg", children: "Metadata" }),
629
+ /* @__PURE__ */ jsx("div", { className: "space-y-2", children: Object.entries(request.metadata).map(([key, value]) => /* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
630
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: key }),
631
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "font-medium", children: typeof value === "string" ? value : JSON.stringify(value) })
632
+ ] }, key)) })
633
+ ] })
634
+ ] }) });
635
+ };
636
+ const config = defineRouteConfig({
637
+ label: "Contact Request Details",
638
+ icon: ChatBubbleLeftRight
639
+ });
171
640
  const en = {};
172
641
  const i18nTranslations0 = {
173
642
  en
@@ -178,15 +647,35 @@ const routeModule = {
178
647
  {
179
648
  Component: ContactEmailSubscriptionsPage,
180
649
  path: "/contact-email-subscriptions"
650
+ },
651
+ {
652
+ Component: ContactRequestsPage,
653
+ path: "/contact-requests"
654
+ },
655
+ {
656
+ Component: ContactRequestDetailPage,
657
+ path: "/contact-requests/:id"
181
658
  }
182
659
  ]
183
660
  };
184
661
  const menuItemModule = {
185
662
  menuItems: [
663
+ {
664
+ label: config$2.label,
665
+ icon: config$2.icon,
666
+ path: "/contact-email-subscriptions",
667
+ nested: void 0
668
+ },
669
+ {
670
+ label: config$1.label,
671
+ icon: config$1.icon,
672
+ path: "/contact-requests",
673
+ nested: void 0
674
+ },
186
675
  {
187
676
  label: config.label,
188
677
  icon: config.icon,
189
- path: "/contact-email-subscriptions",
678
+ path: "/contact-requests/:id",
190
679
  nested: void 0
191
680
  }
192
681
  ]
package/README.md CHANGED
@@ -428,7 +428,7 @@ export async function action(formData: FormData) {
428
428
  ### Email subscriptions
429
429
 
430
430
  ```ts
431
- import { upsertContactSubscription } from "medusa-contact-us"
431
+ import { upsertContactSubscription } from "medusa-contact-us/helpers"
432
432
 
433
433
  await upsertContactSubscription(
434
434
  {
@@ -447,7 +447,7 @@ Using a Medusa JS client keeps credentials in one place while still letting you
447
447
 
448
448
  ```ts
449
449
  import Medusa from "@medusajs/medusa-js"
450
- import { upsertContactSubscription } from "medusa-contact-us"
450
+ import { upsertContactSubscription } from "medusa-contact-us/helpers"
451
451
 
452
452
  const medusa = new Medusa({
453
453
  baseUrl: "https://store.myshop.com",
@@ -472,7 +472,7 @@ await upsertContactSubscription(
472
472
  For SSR or edge runtimes, preconfigure the helper once:
473
473
 
474
474
  ```ts
475
- import { createUpsertContactSubscription } from "medusa-contact-us"
475
+ import { createUpsertContactSubscription } from "medusa-contact-us/helpers"
476
476
 
477
477
  export const upsertSubscription = createUpsertContactSubscription({
478
478
  baseUrl: process.env.NEXT_PUBLIC_MEDUSA_URL,
@@ -482,8 +482,8 @@ export const upsertSubscription = createUpsertContactSubscription({
482
482
  export async function action(formData: FormData) {
483
483
  await upsertSubscription({
484
484
  email: formData.get("email") as string,
485
- status: input.unsubscribe ? "unsubscribed" : "subscribed",
486
- source: input.source ?? "footer_form",
485
+ status: formData.get("unsubscribe") ? "unsubscribed" : "subscribed",
486
+ source: "footer_form",
487
487
  })
488
488
  }
489
489
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "medusa-contact-us",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
4
4
  "description": "Manage storefront email subscriptions (opt-ins and opt-outs) in Medusa Admin.",
5
5
  "author": "Medusa (https://medusajs.com)",
6
6
  "license": "MIT",