medusa-contact-us 0.0.20 → 0.0.22

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,9 @@ 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
+ require("@medusajs/admin-shared");
9
+ const useDebounce$1 = (value, delay) => {
8
10
  const [debouncedValue, setDebouncedValue] = react.useState(value);
9
11
  react.useEffect(() => {
10
12
  const handler = setTimeout(() => setDebouncedValue(value), delay);
@@ -27,7 +29,7 @@ const ContactEmailSubscriptionsPage = () => {
27
29
  const [items, setItems] = react.useState([]);
28
30
  const [statusFilter, setStatusFilter] = react.useState("all");
29
31
  const [query, setQuery] = react.useState("");
30
- const debouncedQuery = useDebounce(query, 300);
32
+ const debouncedQuery = useDebounce$1(query, 300);
31
33
  const [isLoading, setIsLoading] = react.useState(true);
32
34
  const [isFetchingMore, setIsFetchingMore] = react.useState(false);
33
35
  const [error, setError] = react.useState(null);
@@ -165,10 +167,777 @@ const ContactEmailSubscriptionsPage = () => {
165
167
  ) }) : null
166
168
  ] }) });
167
169
  };
168
- const config = adminSdk.defineRouteConfig({
170
+ const config$2 = adminSdk.defineRouteConfig({
169
171
  label: "Contact email list",
170
172
  icon: icons.Envelope
171
173
  });
174
+ const useDebounce = (value, delay) => {
175
+ const [debouncedValue, setDebouncedValue] = react.useState(value);
176
+ react.useEffect(() => {
177
+ const handler = setTimeout(() => setDebouncedValue(value), delay);
178
+ return () => clearTimeout(handler);
179
+ }, [value, delay]);
180
+ return debouncedValue;
181
+ };
182
+ const getStatusBadgeClass$1 = (status) => {
183
+ const statusLower = status.toLowerCase();
184
+ if (statusLower === "pending") {
185
+ return "bg-ui-tag-orange-bg text-ui-tag-orange-text";
186
+ }
187
+ if (statusLower === "in_progress") {
188
+ return "bg-ui-tag-blue-bg text-ui-tag-blue-text";
189
+ }
190
+ if (statusLower === "resolved") {
191
+ return "bg-ui-tag-green-bg text-ui-tag-green-text";
192
+ }
193
+ if (statusLower === "closed") {
194
+ return "bg-ui-tag-grey-bg text-ui-tag-grey-text";
195
+ }
196
+ return "bg-ui-tag-purple-bg text-ui-tag-purple-text";
197
+ };
198
+ const ContactRequestsPage = () => {
199
+ const navigate = reactRouterDom.useNavigate();
200
+ const [items, setItems] = react.useState([]);
201
+ const [statusFilter, setStatusFilter] = react.useState("all");
202
+ const [emailQuery, setEmailQuery] = react.useState("");
203
+ const [sourceFilter, setSourceFilter] = react.useState("all");
204
+ const debouncedEmailQuery = useDebounce(emailQuery, 300);
205
+ const [isLoading, setIsLoading] = react.useState(true);
206
+ const [isFetchingMore, setIsFetchingMore] = react.useState(false);
207
+ const [error, setError] = react.useState(null);
208
+ const [offset, setOffset] = react.useState(0);
209
+ const [count, setCount] = react.useState(0);
210
+ const limit = 50;
211
+ const loadRequests = react.useCallback(
212
+ async (nextOffset, replace = false) => {
213
+ var _a;
214
+ try {
215
+ if (replace) {
216
+ setIsLoading(true);
217
+ } else {
218
+ setIsFetchingMore(true);
219
+ }
220
+ setError(null);
221
+ const params = new URLSearchParams();
222
+ params.set("limit", String(limit));
223
+ params.set("offset", String(nextOffset));
224
+ if (statusFilter !== "all") {
225
+ params.set("status", statusFilter);
226
+ }
227
+ if (debouncedEmailQuery.trim()) {
228
+ params.set("email", debouncedEmailQuery.trim());
229
+ }
230
+ if (sourceFilter !== "all") {
231
+ params.set("source", sourceFilter);
232
+ }
233
+ params.set("order", "created_at");
234
+ params.set("order_direction", "DESC");
235
+ const response = await fetch(
236
+ `/admin/contact-requests?${params.toString()}`,
237
+ { credentials: "include" }
238
+ );
239
+ if (!response.ok) {
240
+ const message = await response.text();
241
+ throw new Error(message || "Unable to load contact requests");
242
+ }
243
+ const payload = await response.json();
244
+ setCount(payload.count ?? 0);
245
+ setOffset(nextOffset + (((_a = payload.requests) == null ? void 0 : _a.length) ?? 0));
246
+ setItems(
247
+ (prev) => replace ? payload.requests ?? [] : [...prev, ...payload.requests ?? []]
248
+ );
249
+ } catch (loadError) {
250
+ const message = loadError instanceof Error ? loadError.message : "Unable to load contact requests";
251
+ setError(message);
252
+ } finally {
253
+ setIsLoading(false);
254
+ setIsFetchingMore(false);
255
+ }
256
+ },
257
+ [statusFilter, debouncedEmailQuery, sourceFilter]
258
+ );
259
+ react.useEffect(() => {
260
+ void loadRequests(0, true);
261
+ }, [statusFilter, debouncedEmailQuery, sourceFilter, loadRequests]);
262
+ const hasMore = react.useMemo(() => offset < count, [offset, count]);
263
+ const availableStatuses = react.useMemo(() => {
264
+ const statuses = /* @__PURE__ */ new Set();
265
+ items.forEach((item) => statuses.add(item.status));
266
+ return Array.from(statuses).sort();
267
+ }, [items]);
268
+ const availableSources = react.useMemo(() => {
269
+ const sources = /* @__PURE__ */ new Set();
270
+ items.forEach((item) => {
271
+ if (item.source) {
272
+ sources.add(item.source);
273
+ }
274
+ });
275
+ return Array.from(sources).sort();
276
+ }, [items]);
277
+ 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: [
278
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
279
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
280
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Contact Requests" }),
281
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Manage and track customer contact requests from the storefront" })
282
+ ] }),
283
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "primary", onClick: () => loadRequests(0, true), children: "Refresh" })
284
+ ] }),
285
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
286
+ /* @__PURE__ */ jsxRuntime.jsx(
287
+ ui.Input,
288
+ {
289
+ placeholder: "Search by email",
290
+ value: emailQuery,
291
+ onChange: (event) => setEmailQuery(event.target.value),
292
+ className: "md:max-w-sm"
293
+ }
294
+ ),
295
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-3", children: [
296
+ /* @__PURE__ */ jsxRuntime.jsxs(
297
+ "select",
298
+ {
299
+ value: statusFilter,
300
+ onChange: (event) => setStatusFilter(event.target.value),
301
+ 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",
302
+ children: [
303
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All Statuses" }),
304
+ availableStatuses.map((status) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: status, children: status.replace("_", " ").toUpperCase() }, status))
305
+ ]
306
+ }
307
+ ),
308
+ /* @__PURE__ */ jsxRuntime.jsxs(
309
+ "select",
310
+ {
311
+ value: sourceFilter,
312
+ onChange: (event) => setSourceFilter(event.target.value),
313
+ 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",
314
+ children: [
315
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All Sources" }),
316
+ availableSources.map((source) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: source, children: source }, source))
317
+ ]
318
+ }
319
+ )
320
+ ] })
321
+ ] }),
322
+ error ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-strong p-6 text-center", children: [
323
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "plus", className: "text-ui-fg-error", children: error }),
324
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4 flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
325
+ ui.Button,
326
+ {
327
+ variant: "secondary",
328
+ onClick: () => loadRequests(0, true),
329
+ children: "Try again"
330
+ }
331
+ ) })
332
+ ] }) : null,
333
+ 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: [
334
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", className: "text-xl", children: "No contact requests yet" }),
335
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "mt-2 text-ui-fg-subtle", children: "Contact requests created through the storefront will appear here." })
336
+ ] }) : /* @__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: [
337
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
338
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Email" }),
339
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Status" }),
340
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Assigned To" }),
341
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Source" }),
342
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Created" }),
343
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Actions" })
344
+ ] }) }),
345
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "divide-y divide-ui-border-subtle", children: items.map((request) => /* @__PURE__ */ jsxRuntime.jsxs(
346
+ "tr",
347
+ {
348
+ className: "hover:bg-ui-bg-subtle/60 cursor-pointer",
349
+ onClick: () => navigate(`/contact-requests/${request.id}`),
350
+ children: [
351
+ /* @__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: [
352
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: request.email }),
353
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: request.id })
354
+ ] }) }),
355
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(
356
+ ui.Badge,
357
+ {
358
+ size: "2xsmall",
359
+ className: `uppercase ${getStatusBadgeClass$1(request.status)}`,
360
+ children: request.status.replace("_", " ")
361
+ }
362
+ ) }),
363
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: request.assign_to ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", children: request.assign_to }) : /* @__PURE__ */ jsxRuntime.jsx("span", { children: "—" }) }),
364
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: request.source ?? "storefront" }),
365
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: new Date(request.created_at).toLocaleString() }),
366
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(
367
+ ui.Button,
368
+ {
369
+ variant: "transparent",
370
+ size: "small",
371
+ onClick: (e) => {
372
+ e.stopPropagation();
373
+ navigate(`/contact-requests/${request.id}`);
374
+ },
375
+ children: "View"
376
+ }
377
+ ) })
378
+ ]
379
+ },
380
+ request.id
381
+ )) })
382
+ ] }) }),
383
+ hasMore ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
384
+ ui.Button,
385
+ {
386
+ variant: "secondary",
387
+ isLoading: isFetchingMore,
388
+ onClick: () => loadRequests(offset, false),
389
+ children: "Load more"
390
+ }
391
+ ) }) : null
392
+ ] }) });
393
+ };
394
+ const config$1 = adminSdk.defineRouteConfig({
395
+ label: "Contact Requests",
396
+ icon: icons.ChatBubbleLeftRight
397
+ });
398
+ const getStatusBadgeClass = (status) => {
399
+ const statusLower = status.toLowerCase();
400
+ if (statusLower === "pending") {
401
+ return "bg-ui-tag-orange-bg text-ui-tag-orange-text";
402
+ }
403
+ if (statusLower === "in_progress") {
404
+ return "bg-ui-tag-blue-bg text-ui-tag-blue-text";
405
+ }
406
+ if (statusLower === "resolved") {
407
+ return "bg-ui-tag-green-bg text-ui-tag-green-text";
408
+ }
409
+ if (statusLower === "closed") {
410
+ return "bg-ui-tag-grey-bg text-ui-tag-grey-text";
411
+ }
412
+ return "bg-ui-tag-purple-bg text-ui-tag-purple-text";
413
+ };
414
+ const ContactRequestDetailPage = () => {
415
+ const navigate = reactRouterDom.useNavigate();
416
+ const { id } = reactRouterDom.useParams();
417
+ const [request, setRequest] = react.useState(null);
418
+ const [comments, setComments] = react.useState([]);
419
+ const [nextAllowedStatuses, setNextAllowedStatuses] = react.useState([]);
420
+ const [selectedStatus, setSelectedStatus] = react.useState("");
421
+ const [isLoading, setIsLoading] = react.useState(true);
422
+ const [isUpdating, setIsUpdating] = react.useState(false);
423
+ const [error, setError] = react.useState(null);
424
+ const [updateError, setUpdateError] = react.useState(null);
425
+ const [updateSuccess, setUpdateSuccess] = react.useState(false);
426
+ const [selectedAssignTo, setSelectedAssignTo] = react.useState("");
427
+ const [isAssigning, setIsAssigning] = react.useState(false);
428
+ const [assignError, setAssignError] = react.useState(null);
429
+ const [assignSuccess, setAssignSuccess] = react.useState(false);
430
+ const [commentText, setCommentText] = react.useState("");
431
+ const [commentImages, setCommentImages] = react.useState([]);
432
+ const [isCreatingComment, setIsCreatingComment] = react.useState(false);
433
+ const [commentError, setCommentError] = react.useState(null);
434
+ const [commentSuccess, setCommentSuccess] = react.useState(false);
435
+ react.useEffect(() => {
436
+ if (!id) {
437
+ navigate("/contact-requests");
438
+ return;
439
+ }
440
+ const loadRequest = async () => {
441
+ try {
442
+ setIsLoading(true);
443
+ setError(null);
444
+ const response = await fetch(`/admin/contact-requests/${id}`, {
445
+ credentials: "include"
446
+ });
447
+ if (!response.ok) {
448
+ const message = await response.text();
449
+ throw new Error(message || "Unable to load contact request");
450
+ }
451
+ const payload = await response.json();
452
+ setRequest(payload.request);
453
+ setComments(payload.comments ?? []);
454
+ setNextAllowedStatuses(payload.next_allowed_statuses ?? []);
455
+ setSelectedStatus("");
456
+ setSelectedAssignTo("");
457
+ } catch (loadError) {
458
+ const message = loadError instanceof Error ? loadError.message : "Unable to load contact request";
459
+ setError(message);
460
+ } finally {
461
+ setIsLoading(false);
462
+ }
463
+ };
464
+ void loadRequest();
465
+ }, [id, navigate]);
466
+ const handleStatusUpdate = async () => {
467
+ if (!id || !selectedStatus) {
468
+ return;
469
+ }
470
+ try {
471
+ setIsUpdating(true);
472
+ setUpdateError(null);
473
+ setUpdateSuccess(false);
474
+ const response = await fetch(`/admin/contact-requests/${id}/status`, {
475
+ method: "POST",
476
+ headers: {
477
+ "Content-Type": "application/json"
478
+ },
479
+ credentials: "include",
480
+ body: JSON.stringify({ status: selectedStatus })
481
+ });
482
+ if (!response.ok) {
483
+ const message = await response.text();
484
+ throw new Error(message || "Unable to update status");
485
+ }
486
+ const payload = await response.json();
487
+ setRequest(payload.request);
488
+ setSelectedStatus("");
489
+ setUpdateSuccess(true);
490
+ setTimeout(() => setUpdateSuccess(false), 3e3);
491
+ const detailResponse = await fetch(`/admin/contact-requests/${id}`, {
492
+ credentials: "include"
493
+ });
494
+ if (detailResponse.ok) {
495
+ const detailPayload = await detailResponse.json();
496
+ setRequest(detailPayload.request);
497
+ setComments(detailPayload.comments ?? []);
498
+ setNextAllowedStatuses(detailPayload.next_allowed_statuses ?? []);
499
+ }
500
+ } catch (updateErr) {
501
+ const message = updateErr instanceof Error ? updateErr.message : "Unable to update status";
502
+ setUpdateError(message);
503
+ } finally {
504
+ setIsUpdating(false);
505
+ }
506
+ };
507
+ const handleAssign = async () => {
508
+ if (!id) {
509
+ return;
510
+ }
511
+ try {
512
+ setIsAssigning(true);
513
+ setAssignError(null);
514
+ setAssignSuccess(false);
515
+ const response = await fetch(`/admin/contact-requests/${id}/assign`, {
516
+ method: "POST",
517
+ headers: {
518
+ "Content-Type": "application/json"
519
+ },
520
+ credentials: "include",
521
+ body: JSON.stringify({
522
+ assign_to: selectedAssignTo.trim() || null
523
+ })
524
+ });
525
+ if (!response.ok) {
526
+ const message = await response.text();
527
+ throw new Error(message || "Unable to assign contact request");
528
+ }
529
+ const payload = await response.json();
530
+ setRequest(payload.request);
531
+ setSelectedAssignTo("");
532
+ setAssignSuccess(true);
533
+ setTimeout(() => setAssignSuccess(false), 3e3);
534
+ const detailResponse = await fetch(`/admin/contact-requests/${id}`, {
535
+ credentials: "include"
536
+ });
537
+ if (detailResponse.ok) {
538
+ const detailPayload = await detailResponse.json();
539
+ setRequest(detailPayload.request);
540
+ setComments(detailPayload.comments ?? []);
541
+ setNextAllowedStatuses(detailPayload.next_allowed_statuses ?? []);
542
+ }
543
+ } catch (assignErr) {
544
+ const message = assignErr instanceof Error ? assignErr.message : "Unable to assign contact request";
545
+ setAssignError(message);
546
+ } finally {
547
+ setIsAssigning(false);
548
+ }
549
+ };
550
+ const handleUnassign = async () => {
551
+ if (!id) {
552
+ return;
553
+ }
554
+ try {
555
+ setIsAssigning(true);
556
+ setAssignError(null);
557
+ setAssignSuccess(false);
558
+ const response = await fetch(`/admin/contact-requests/${id}/assign`, {
559
+ method: "POST",
560
+ headers: {
561
+ "Content-Type": "application/json"
562
+ },
563
+ credentials: "include",
564
+ body: JSON.stringify({
565
+ assign_to: null
566
+ })
567
+ });
568
+ if (!response.ok) {
569
+ const message = await response.text();
570
+ throw new Error(message || "Unable to unassign contact request");
571
+ }
572
+ const payload = await response.json();
573
+ setRequest(payload.request);
574
+ setAssignSuccess(true);
575
+ setTimeout(() => setAssignSuccess(false), 3e3);
576
+ const detailResponse = await fetch(`/admin/contact-requests/${id}`, {
577
+ credentials: "include"
578
+ });
579
+ if (detailResponse.ok) {
580
+ const detailPayload = await detailResponse.json();
581
+ setRequest(detailPayload.request);
582
+ setComments(detailPayload.comments ?? []);
583
+ setNextAllowedStatuses(detailPayload.next_allowed_statuses ?? []);
584
+ }
585
+ } catch (assignErr) {
586
+ const message = assignErr instanceof Error ? assignErr.message : "Unable to unassign contact request";
587
+ setAssignError(message);
588
+ } finally {
589
+ setIsAssigning(false);
590
+ }
591
+ };
592
+ const handleCreateComment = async () => {
593
+ if (!id || !commentText.trim() && commentImages.length === 0) {
594
+ setCommentError("Please provide a comment or at least one image");
595
+ return;
596
+ }
597
+ try {
598
+ setIsCreatingComment(true);
599
+ setCommentError(null);
600
+ setCommentSuccess(false);
601
+ const response = await fetch(`/admin/contact-requests/${id}/comments`, {
602
+ method: "POST",
603
+ headers: {
604
+ "Content-Type": "application/json"
605
+ },
606
+ credentials: "include",
607
+ body: JSON.stringify({
608
+ comment: commentText.trim() || void 0,
609
+ images: commentImages.filter((img) => img.trim()).length > 0 ? commentImages.filter((img) => img.trim()) : void 0
610
+ })
611
+ });
612
+ if (!response.ok) {
613
+ const message = await response.text();
614
+ throw new Error(message || "Unable to create comment");
615
+ }
616
+ const detailResponse = await fetch(`/admin/contact-requests/${id}`, {
617
+ credentials: "include"
618
+ });
619
+ if (detailResponse.ok) {
620
+ const detailPayload = await detailResponse.json();
621
+ setComments(detailPayload.comments ?? []);
622
+ }
623
+ setCommentText("");
624
+ setCommentImages([]);
625
+ setCommentSuccess(true);
626
+ setTimeout(() => setCommentSuccess(false), 3e3);
627
+ } catch (commentErr) {
628
+ const message = commentErr instanceof Error ? commentErr.message : "Unable to create comment";
629
+ setCommentError(message);
630
+ } finally {
631
+ setIsCreatingComment(false);
632
+ }
633
+ };
634
+ const addImageInput = () => {
635
+ setCommentImages([...commentImages, ""]);
636
+ };
637
+ const removeImageInput = (index) => {
638
+ setCommentImages(commentImages.filter((_, i) => i !== index));
639
+ };
640
+ const updateImageUrl = (index, value) => {
641
+ const updated = [...commentImages];
642
+ updated[index] = value;
643
+ setCommentImages(updated);
644
+ };
645
+ if (isLoading) {
646
+ 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..." }) }) }) });
647
+ }
648
+ if (error || !request) {
649
+ 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: [
650
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "plus", className: "text-ui-fg-error", children: error || "Contact request not found" }),
651
+ /* @__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" }) })
652
+ ] }) }) });
653
+ }
654
+ 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: [
655
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-3", children: [
656
+ /* @__PURE__ */ jsxRuntime.jsxs(
657
+ ui.Button,
658
+ {
659
+ variant: "transparent",
660
+ size: "small",
661
+ onClick: () => navigate("/contact-requests"),
662
+ className: "w-fit",
663
+ children: [
664
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowLeft, { className: "mr-2" }),
665
+ "Back to list"
666
+ ]
667
+ }
668
+ ),
669
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1 md:flex-row md:items-center md:justify-between", children: [
670
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
671
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Contact Request Details" }),
672
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: request.id })
673
+ ] }),
674
+ /* @__PURE__ */ jsxRuntime.jsx(
675
+ ui.Badge,
676
+ {
677
+ size: "small",
678
+ className: `uppercase ${getStatusBadgeClass(request.status)}`,
679
+ children: request.status.replace("_", " ")
680
+ }
681
+ )
682
+ ] })
683
+ ] }),
684
+ nextAllowedStatuses.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
685
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Update Status" }),
686
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-end", children: [
687
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
688
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "mb-2 block text-sm font-medium text-ui-fg-base", children: "New Status" }),
689
+ /* @__PURE__ */ jsxRuntime.jsxs(
690
+ "select",
691
+ {
692
+ value: selectedStatus,
693
+ onChange: (event) => setSelectedStatus(event.target.value),
694
+ 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",
695
+ children: [
696
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select new status" }),
697
+ nextAllowedStatuses.map((status) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: status, children: status.replace("_", " ").toUpperCase() }, status))
698
+ ]
699
+ }
700
+ )
701
+ ] }),
702
+ /* @__PURE__ */ jsxRuntime.jsx(
703
+ ui.Button,
704
+ {
705
+ variant: "primary",
706
+ onClick: handleStatusUpdate,
707
+ disabled: !selectedStatus || isUpdating,
708
+ isLoading: isUpdating,
709
+ children: "Update Status"
710
+ }
711
+ )
712
+ ] }),
713
+ updateError && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "mt-2 text-ui-fg-error", children: updateError }),
714
+ updateSuccess && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "mt-2 text-ui-fg-success", children: "Status updated successfully" })
715
+ ] }),
716
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
717
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Assignment" }),
718
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
719
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
720
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Currently Assigned To" }),
721
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: request.assign_to ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "small", className: "mt-1", children: request.assign_to }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-ui-fg-subtle", children: "Unassigned" }) })
722
+ ] }),
723
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-end", children: [
724
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
725
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "mb-2 block text-sm font-medium text-ui-fg-base", children: "Admin User ID" }),
726
+ /* @__PURE__ */ jsxRuntime.jsx(
727
+ ui.Input,
728
+ {
729
+ type: "text",
730
+ value: selectedAssignTo,
731
+ onChange: (event) => setSelectedAssignTo(event.target.value),
732
+ placeholder: "Enter admin user ID",
733
+ className: "w-full"
734
+ }
735
+ )
736
+ ] }),
737
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
738
+ /* @__PURE__ */ jsxRuntime.jsx(
739
+ ui.Button,
740
+ {
741
+ variant: "primary",
742
+ onClick: handleAssign,
743
+ disabled: isAssigning,
744
+ isLoading: isAssigning,
745
+ children: "Assign"
746
+ }
747
+ ),
748
+ request.assign_to && /* @__PURE__ */ jsxRuntime.jsx(
749
+ ui.Button,
750
+ {
751
+ variant: "secondary",
752
+ onClick: handleUnassign,
753
+ disabled: isAssigning,
754
+ isLoading: isAssigning,
755
+ children: "Unassign"
756
+ }
757
+ )
758
+ ] })
759
+ ] }),
760
+ assignError && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: assignError }),
761
+ assignSuccess && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-success", children: "Assignment updated successfully" })
762
+ ] })
763
+ ] }),
764
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-6 md:grid-cols-2", children: [
765
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
766
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Contact Information" }),
767
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
768
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
769
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Email" }),
770
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: request.email })
771
+ ] }),
772
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
773
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Source" }),
774
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: request.source ?? "storefront" })
775
+ ] }),
776
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
777
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Created" }),
778
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: new Date(request.created_at).toLocaleString() })
779
+ ] }),
780
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
781
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Last Updated" }),
782
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium", children: new Date(request.updated_at).toLocaleString() })
783
+ ] })
784
+ ] })
785
+ ] }),
786
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
787
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Status History" }),
788
+ 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(
789
+ "div",
790
+ {
791
+ className: "flex items-center justify-between border-b border-ui-border-subtle pb-2 last:border-0",
792
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
793
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
794
+ /* @__PURE__ */ jsxRuntime.jsx(
795
+ ui.Badge,
796
+ {
797
+ size: "2xsmall",
798
+ className: `uppercase ${getStatusBadgeClass(entry.to)}`,
799
+ children: entry.to.replace("_", " ")
800
+ }
801
+ ),
802
+ entry.from && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
803
+ "from ",
804
+ entry.from.replace("_", " ")
805
+ ] })
806
+ ] }),
807
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
808
+ new Date(entry.changed_at).toLocaleString(),
809
+ entry.changed_by && ` by ${entry.changed_by}`
810
+ ] })
811
+ ] })
812
+ },
813
+ index
814
+ )) }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "No status history available" })
815
+ ] })
816
+ ] }),
817
+ 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: [
818
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Request Details" }),
819
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-3", children: Object.entries(request.payload).map(([key, value]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
820
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: key.charAt(0).toUpperCase() + key.slice(1).replace("_", " ") }),
821
+ /* @__PURE__ */ jsxRuntime.jsx(
822
+ ui.Textarea,
823
+ {
824
+ value: typeof value === "string" ? value : JSON.stringify(value, null, 2),
825
+ readOnly: true,
826
+ className: "mt-1 min-h-[60px]"
827
+ }
828
+ )
829
+ ] }, key)) })
830
+ ] }),
831
+ 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: [
832
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Metadata" }),
833
+ /* @__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: [
834
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: key }),
835
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "font-medium", children: typeof value === "string" ? value : JSON.stringify(value) })
836
+ ] }, key)) })
837
+ ] }),
838
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
839
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mb-4 text-lg", children: "Comments" }),
840
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6 space-y-4 rounded-lg border border-ui-border-subtle bg-ui-bg-subtle p-4", children: [
841
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
842
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "mb-2 block text-sm font-medium text-ui-fg-base", children: "Add Comment" }),
843
+ /* @__PURE__ */ jsxRuntime.jsx(
844
+ ui.Textarea,
845
+ {
846
+ value: commentText,
847
+ onChange: (event) => setCommentText(event.target.value),
848
+ placeholder: "Enter your comment...",
849
+ className: "min-h-[80px]"
850
+ }
851
+ )
852
+ ] }),
853
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
854
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center justify-between", children: [
855
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-sm font-medium text-ui-fg-base", children: "Images (URLs)" }),
856
+ /* @__PURE__ */ jsxRuntime.jsx(
857
+ ui.Button,
858
+ {
859
+ variant: "transparent",
860
+ size: "small",
861
+ onClick: addImageInput,
862
+ type: "button",
863
+ children: "Add Image URL"
864
+ }
865
+ )
866
+ ] }),
867
+ commentImages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: commentImages.map((url, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
868
+ /* @__PURE__ */ jsxRuntime.jsx(
869
+ ui.Input,
870
+ {
871
+ type: "text",
872
+ value: url,
873
+ onChange: (event) => updateImageUrl(index, event.target.value),
874
+ placeholder: "https://example.com/image.jpg",
875
+ className: "flex-1"
876
+ }
877
+ ),
878
+ /* @__PURE__ */ jsxRuntime.jsx(
879
+ ui.Button,
880
+ {
881
+ variant: "transparent",
882
+ size: "small",
883
+ onClick: () => removeImageInput(index),
884
+ type: "button",
885
+ children: "Remove"
886
+ }
887
+ )
888
+ ] }, index)) })
889
+ ] }),
890
+ /* @__PURE__ */ jsxRuntime.jsx(
891
+ ui.Button,
892
+ {
893
+ variant: "primary",
894
+ onClick: handleCreateComment,
895
+ disabled: isCreatingComment || !commentText.trim() && commentImages.filter((img) => img.trim()).length === 0,
896
+ isLoading: isCreatingComment,
897
+ children: "Add Comment"
898
+ }
899
+ ),
900
+ commentError && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: commentError }),
901
+ commentSuccess && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-success", children: "Comment added successfully" })
902
+ ] }),
903
+ comments.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-4", children: comments.map((comment) => /* @__PURE__ */ jsxRuntime.jsxs(
904
+ "div",
905
+ {
906
+ className: "rounded-lg border border-ui-border-subtle bg-ui-bg-subtle p-4",
907
+ children: [
908
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2 flex items-center justify-between", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
909
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", children: comment.admin_id }),
910
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: new Date(comment.created_at).toLocaleString() })
911
+ ] }) }),
912
+ comment.comment && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "mb-2 whitespace-pre-wrap", children: comment.comment }),
913
+ comment.images && comment.images.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 space-y-2", children: [
914
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Images:" }),
915
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: comment.images.map((imageUrl, index) => /* @__PURE__ */ jsxRuntime.jsxs(
916
+ "a",
917
+ {
918
+ href: imageUrl,
919
+ target: "_blank",
920
+ rel: "noopener noreferrer",
921
+ className: "text-sm text-ui-fg-interactive hover:underline",
922
+ children: [
923
+ "Image ",
924
+ index + 1
925
+ ]
926
+ },
927
+ index
928
+ )) })
929
+ ] })
930
+ ]
931
+ },
932
+ comment.id
933
+ )) }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "No comments yet" })
934
+ ] })
935
+ ] }) });
936
+ };
937
+ const config = adminSdk.defineRouteConfig({
938
+ label: "Contact Request Details",
939
+ icon: icons.ChatBubbleLeftRight
940
+ });
172
941
  const en = {};
173
942
  const i18nTranslations0 = {
174
943
  en
@@ -179,15 +948,35 @@ const routeModule = {
179
948
  {
180
949
  Component: ContactEmailSubscriptionsPage,
181
950
  path: "/contact-email-subscriptions"
951
+ },
952
+ {
953
+ Component: ContactRequestsPage,
954
+ path: "/contact-requests"
955
+ },
956
+ {
957
+ Component: ContactRequestDetailPage,
958
+ path: "/contact-requests/:id"
182
959
  }
183
960
  ]
184
961
  };
185
962
  const menuItemModule = {
186
963
  menuItems: [
964
+ {
965
+ label: config$2.label,
966
+ icon: config$2.icon,
967
+ path: "/contact-email-subscriptions",
968
+ nested: void 0
969
+ },
970
+ {
971
+ label: config$1.label,
972
+ icon: config$1.icon,
973
+ path: "/contact-requests",
974
+ nested: void 0
975
+ },
187
976
  {
188
977
  label: config.label,
189
978
  icon: config.icon,
190
- path: "/contact-email-subscriptions",
979
+ path: "/contact-requests/:id",
191
980
  nested: void 0
192
981
  }
193
982
  ]