medusa-contact-us 0.0.21 → 0.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/admin/index.js +446 -0
- package/.medusa/server/src/admin/index.mjs +447 -1
- package/.medusa/server/src/api/admin/contact-requests/[id]/assign/route.js +54 -0
- package/.medusa/server/src/api/admin/contact-requests/[id]/comments/route.js +28 -0
- package/.medusa/server/src/api/admin/contact-requests/[id]/route.js +36 -9
- package/.medusa/server/src/api/admin/contact-requests/validators.js +9 -2
- package/.medusa/server/src/api/admin/users/route.js +70 -0
- package/.medusa/server/src/modules/contact-requests/index.js +4 -2
- package/.medusa/server/src/modules/contact-requests/migrations/Migration20241129163317.js +2 -2
- package/.medusa/server/src/modules/contact-requests/migrations/Migration20251222161335.js +27 -0
- package/.medusa/server/src/modules/contact-requests/migrations/Migration20251222161336.js +37 -0
- package/.medusa/server/src/modules/contact-requests/models/contact-request-comment.js +13 -0
- package/.medusa/server/src/modules/contact-requests/models/contact-request.js +2 -1
- package/.medusa/server/src/modules/contact-requests/service.js +92 -1
- package/.medusa/server/src/modules/contact-subscriptions/migrations/Migration20241126103000.js +2 -2
- package/package.json +8 -5
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
2
|
+
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
|
3
3
|
import { defineRouteConfig } from "@medusajs/admin-sdk";
|
|
4
4
|
import { Container, Heading, Text, Button, Input, Badge, Textarea } from "@medusajs/ui";
|
|
5
5
|
import { Envelope, ChatBubbleLeftRight, ArrowLeft } from "@medusajs/icons";
|
|
6
6
|
import { useNavigate, useParams } from "react-router-dom";
|
|
7
|
+
import "@medusajs/admin-shared";
|
|
7
8
|
const useDebounce$1 = (value, delay) => {
|
|
8
9
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
9
10
|
useEffect(() => {
|
|
@@ -335,6 +336,7 @@ const ContactRequestsPage = () => {
|
|
|
335
336
|
/* @__PURE__ */ jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
336
337
|
/* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Email" }),
|
|
337
338
|
/* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Status" }),
|
|
339
|
+
/* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Assigned To" }),
|
|
338
340
|
/* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Source" }),
|
|
339
341
|
/* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Created" }),
|
|
340
342
|
/* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Actions" })
|
|
@@ -357,6 +359,7 @@ const ContactRequestsPage = () => {
|
|
|
357
359
|
children: request.status.replace("_", " ")
|
|
358
360
|
}
|
|
359
361
|
) }),
|
|
362
|
+
/* @__PURE__ */ jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: request.assign_to ? /* @__PURE__ */ jsx(Badge, { size: "2xsmall", children: request.assign_to }) : /* @__PURE__ */ jsx("span", { children: "—" }) }),
|
|
360
363
|
/* @__PURE__ */ jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: request.source ?? "storefront" }),
|
|
361
364
|
/* @__PURE__ */ jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: new Date(request.created_at).toLocaleString() }),
|
|
362
365
|
/* @__PURE__ */ jsx("td", { className: "px-4 py-4", children: /* @__PURE__ */ jsx(
|
|
@@ -411,6 +414,7 @@ const ContactRequestDetailPage = () => {
|
|
|
411
414
|
const navigate = useNavigate();
|
|
412
415
|
const { id } = useParams();
|
|
413
416
|
const [request, setRequest] = useState(null);
|
|
417
|
+
const [comments, setComments] = useState([]);
|
|
414
418
|
const [nextAllowedStatuses, setNextAllowedStatuses] = useState([]);
|
|
415
419
|
const [selectedStatus, setSelectedStatus] = useState("");
|
|
416
420
|
const [isLoading, setIsLoading] = useState(true);
|
|
@@ -418,6 +422,58 @@ const ContactRequestDetailPage = () => {
|
|
|
418
422
|
const [error, setError] = useState(null);
|
|
419
423
|
const [updateError, setUpdateError] = useState(null);
|
|
420
424
|
const [updateSuccess, setUpdateSuccess] = useState(false);
|
|
425
|
+
const [selectedAssignTo, setSelectedAssignTo] = useState("");
|
|
426
|
+
const [isAssigning, setIsAssigning] = useState(false);
|
|
427
|
+
const [assignError, setAssignError] = useState(null);
|
|
428
|
+
const [assignSuccess, setAssignSuccess] = useState(false);
|
|
429
|
+
const [adminUsers, setAdminUsers] = useState([]);
|
|
430
|
+
const [adminSearchQuery, setAdminSearchQuery] = useState("");
|
|
431
|
+
const [isAdminDropdownOpen, setIsAdminDropdownOpen] = useState(false);
|
|
432
|
+
const [isLoadingAdmins, setIsLoadingAdmins] = useState(false);
|
|
433
|
+
const [selectedAdminIndex, setSelectedAdminIndex] = useState(-1);
|
|
434
|
+
const adminDropdownRef = useRef(null);
|
|
435
|
+
const adminInputRef = useRef(null);
|
|
436
|
+
const [commentText, setCommentText] = useState("");
|
|
437
|
+
const [commentImages, setCommentImages] = useState([]);
|
|
438
|
+
const [isCreatingComment, setIsCreatingComment] = useState(false);
|
|
439
|
+
const [commentError, setCommentError] = useState(null);
|
|
440
|
+
const [commentSuccess, setCommentSuccess] = useState(false);
|
|
441
|
+
const useDebounce2 = (value, delay) => {
|
|
442
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
443
|
+
useEffect(() => {
|
|
444
|
+
const handler = setTimeout(() => setDebouncedValue(value), delay);
|
|
445
|
+
return () => clearTimeout(handler);
|
|
446
|
+
}, [value, delay]);
|
|
447
|
+
return debouncedValue;
|
|
448
|
+
};
|
|
449
|
+
const debouncedAdminSearch = useDebounce2(adminSearchQuery, 300);
|
|
450
|
+
const fetchAdminUsers = useCallback(async (searchQuery) => {
|
|
451
|
+
try {
|
|
452
|
+
setIsLoadingAdmins(true);
|
|
453
|
+
const params = new URLSearchParams();
|
|
454
|
+
if (searchQuery) {
|
|
455
|
+
params.set("q", searchQuery);
|
|
456
|
+
}
|
|
457
|
+
const response = await fetch(`/admin/users?${params.toString()}`, {
|
|
458
|
+
credentials: "include"
|
|
459
|
+
});
|
|
460
|
+
if (!response.ok) {
|
|
461
|
+
throw new Error("Unable to load admin users");
|
|
462
|
+
}
|
|
463
|
+
const payload = await response.json();
|
|
464
|
+
setAdminUsers(payload.users || []);
|
|
465
|
+
} catch (fetchError) {
|
|
466
|
+
console.error("Error fetching admin users:", fetchError);
|
|
467
|
+
setAdminUsers([]);
|
|
468
|
+
} finally {
|
|
469
|
+
setIsLoadingAdmins(false);
|
|
470
|
+
}
|
|
471
|
+
}, []);
|
|
472
|
+
const filteredAdmins = adminUsers.filter((admin) => {
|
|
473
|
+
if (!debouncedAdminSearch) return true;
|
|
474
|
+
const query = debouncedAdminSearch.toLowerCase();
|
|
475
|
+
return admin.name.toLowerCase().includes(query) || admin.email.toLowerCase().includes(query) || admin.id.toLowerCase().includes(query);
|
|
476
|
+
});
|
|
421
477
|
useEffect(() => {
|
|
422
478
|
if (!id) {
|
|
423
479
|
navigate("/contact-requests");
|
|
@@ -436,8 +492,11 @@ const ContactRequestDetailPage = () => {
|
|
|
436
492
|
}
|
|
437
493
|
const payload = await response.json();
|
|
438
494
|
setRequest(payload.request);
|
|
495
|
+
setComments(payload.comments ?? []);
|
|
439
496
|
setNextAllowedStatuses(payload.next_allowed_statuses ?? []);
|
|
440
497
|
setSelectedStatus("");
|
|
498
|
+
setSelectedAssignTo("");
|
|
499
|
+
setAdminSearchQuery("");
|
|
441
500
|
} catch (loadError) {
|
|
442
501
|
const message = loadError instanceof Error ? loadError.message : "Unable to load contact request";
|
|
443
502
|
setError(message);
|
|
@@ -447,6 +506,59 @@ const ContactRequestDetailPage = () => {
|
|
|
447
506
|
};
|
|
448
507
|
void loadRequest();
|
|
449
508
|
}, [id, navigate]);
|
|
509
|
+
useEffect(() => {
|
|
510
|
+
if (isAdminDropdownOpen) {
|
|
511
|
+
void fetchAdminUsers(debouncedAdminSearch || void 0);
|
|
512
|
+
}
|
|
513
|
+
}, [isAdminDropdownOpen, debouncedAdminSearch, fetchAdminUsers]);
|
|
514
|
+
useEffect(() => {
|
|
515
|
+
const handleClickOutside = (event) => {
|
|
516
|
+
if (adminDropdownRef.current && !adminDropdownRef.current.contains(event.target) && adminInputRef.current && !adminInputRef.current.contains(event.target)) {
|
|
517
|
+
setIsAdminDropdownOpen(false);
|
|
518
|
+
setSelectedAdminIndex(-1);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
if (isAdminDropdownOpen) {
|
|
522
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
523
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
524
|
+
}
|
|
525
|
+
}, [isAdminDropdownOpen]);
|
|
526
|
+
const handleAdminKeyDown = (event) => {
|
|
527
|
+
if (event.key === "ArrowDown") {
|
|
528
|
+
event.preventDefault();
|
|
529
|
+
setSelectedAdminIndex(
|
|
530
|
+
(prev) => prev < filteredAdmins.length - 1 ? prev + 1 : prev
|
|
531
|
+
);
|
|
532
|
+
setIsAdminDropdownOpen(true);
|
|
533
|
+
} else if (event.key === "ArrowUp") {
|
|
534
|
+
event.preventDefault();
|
|
535
|
+
setSelectedAdminIndex((prev) => prev > 0 ? prev - 1 : -1);
|
|
536
|
+
setIsAdminDropdownOpen(true);
|
|
537
|
+
} else if (event.key === "Enter" && selectedAdminIndex >= 0) {
|
|
538
|
+
event.preventDefault();
|
|
539
|
+
const admin = filteredAdmins[selectedAdminIndex];
|
|
540
|
+
if (admin) {
|
|
541
|
+
handleAdminSelect(admin);
|
|
542
|
+
}
|
|
543
|
+
} else if (event.key === "Escape") {
|
|
544
|
+
setIsAdminDropdownOpen(false);
|
|
545
|
+
setSelectedAdminIndex(-1);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
const handleAdminSelect = (admin) => {
|
|
549
|
+
setSelectedAssignTo(admin.id);
|
|
550
|
+
setAdminSearchQuery(admin.name || admin.email);
|
|
551
|
+
setIsAdminDropdownOpen(false);
|
|
552
|
+
setSelectedAdminIndex(-1);
|
|
553
|
+
};
|
|
554
|
+
useEffect(() => {
|
|
555
|
+
if ((request == null ? void 0 : request.assign_to) && adminUsers.length > 0) {
|
|
556
|
+
const assignedAdmin = adminUsers.find((admin) => admin.id === request.assign_to);
|
|
557
|
+
if (assignedAdmin && !adminSearchQuery) {
|
|
558
|
+
setAdminSearchQuery(assignedAdmin.name || assignedAdmin.email);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}, [request == null ? void 0 : request.assign_to, adminUsers, adminSearchQuery]);
|
|
450
562
|
const handleStatusUpdate = async () => {
|
|
451
563
|
if (!id || !selectedStatus) {
|
|
452
564
|
return;
|
|
@@ -477,6 +589,8 @@ const ContactRequestDetailPage = () => {
|
|
|
477
589
|
});
|
|
478
590
|
if (detailResponse.ok) {
|
|
479
591
|
const detailPayload = await detailResponse.json();
|
|
592
|
+
setRequest(detailPayload.request);
|
|
593
|
+
setComments(detailPayload.comments ?? []);
|
|
480
594
|
setNextAllowedStatuses(detailPayload.next_allowed_statuses ?? []);
|
|
481
595
|
}
|
|
482
596
|
} catch (updateErr) {
|
|
@@ -486,6 +600,144 @@ const ContactRequestDetailPage = () => {
|
|
|
486
600
|
setIsUpdating(false);
|
|
487
601
|
}
|
|
488
602
|
};
|
|
603
|
+
const handleAssign = async () => {
|
|
604
|
+
if (!id) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
setIsAssigning(true);
|
|
609
|
+
setAssignError(null);
|
|
610
|
+
setAssignSuccess(false);
|
|
611
|
+
const response = await fetch(`/admin/contact-requests/${id}/assign`, {
|
|
612
|
+
method: "POST",
|
|
613
|
+
headers: {
|
|
614
|
+
"Content-Type": "application/json"
|
|
615
|
+
},
|
|
616
|
+
credentials: "include",
|
|
617
|
+
body: JSON.stringify({
|
|
618
|
+
assign_to: selectedAssignTo.trim() || null
|
|
619
|
+
})
|
|
620
|
+
});
|
|
621
|
+
if (!response.ok) {
|
|
622
|
+
const message = await response.text();
|
|
623
|
+
throw new Error(message || "Unable to assign contact request");
|
|
624
|
+
}
|
|
625
|
+
const payload = await response.json();
|
|
626
|
+
setRequest(payload.request);
|
|
627
|
+
setSelectedAssignTo("");
|
|
628
|
+
setAssignSuccess(true);
|
|
629
|
+
setTimeout(() => setAssignSuccess(false), 3e3);
|
|
630
|
+
const detailResponse = await fetch(`/admin/contact-requests/${id}`, {
|
|
631
|
+
credentials: "include"
|
|
632
|
+
});
|
|
633
|
+
if (detailResponse.ok) {
|
|
634
|
+
const detailPayload = await detailResponse.json();
|
|
635
|
+
setRequest(detailPayload.request);
|
|
636
|
+
setComments(detailPayload.comments ?? []);
|
|
637
|
+
setNextAllowedStatuses(detailPayload.next_allowed_statuses ?? []);
|
|
638
|
+
}
|
|
639
|
+
} catch (assignErr) {
|
|
640
|
+
const message = assignErr instanceof Error ? assignErr.message : "Unable to assign contact request";
|
|
641
|
+
setAssignError(message);
|
|
642
|
+
} finally {
|
|
643
|
+
setIsAssigning(false);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
const handleUnassign = async () => {
|
|
647
|
+
if (!id) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
setIsAssigning(true);
|
|
652
|
+
setAssignError(null);
|
|
653
|
+
setAssignSuccess(false);
|
|
654
|
+
const response = await fetch(`/admin/contact-requests/${id}/assign`, {
|
|
655
|
+
method: "POST",
|
|
656
|
+
headers: {
|
|
657
|
+
"Content-Type": "application/json"
|
|
658
|
+
},
|
|
659
|
+
credentials: "include",
|
|
660
|
+
body: JSON.stringify({
|
|
661
|
+
assign_to: null
|
|
662
|
+
})
|
|
663
|
+
});
|
|
664
|
+
if (!response.ok) {
|
|
665
|
+
const message = await response.text();
|
|
666
|
+
throw new Error(message || "Unable to unassign contact request");
|
|
667
|
+
}
|
|
668
|
+
const payload = await response.json();
|
|
669
|
+
setRequest(payload.request);
|
|
670
|
+
setAssignSuccess(true);
|
|
671
|
+
setTimeout(() => setAssignSuccess(false), 3e3);
|
|
672
|
+
const detailResponse = await fetch(`/admin/contact-requests/${id}`, {
|
|
673
|
+
credentials: "include"
|
|
674
|
+
});
|
|
675
|
+
if (detailResponse.ok) {
|
|
676
|
+
const detailPayload = await detailResponse.json();
|
|
677
|
+
setRequest(detailPayload.request);
|
|
678
|
+
setComments(detailPayload.comments ?? []);
|
|
679
|
+
setNextAllowedStatuses(detailPayload.next_allowed_statuses ?? []);
|
|
680
|
+
}
|
|
681
|
+
} catch (assignErr) {
|
|
682
|
+
const message = assignErr instanceof Error ? assignErr.message : "Unable to unassign contact request";
|
|
683
|
+
setAssignError(message);
|
|
684
|
+
} finally {
|
|
685
|
+
setIsAssigning(false);
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
const handleCreateComment = async () => {
|
|
689
|
+
if (!id || !commentText.trim() && commentImages.length === 0) {
|
|
690
|
+
setCommentError("Please provide a comment or at least one image");
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
setIsCreatingComment(true);
|
|
695
|
+
setCommentError(null);
|
|
696
|
+
setCommentSuccess(false);
|
|
697
|
+
const response = await fetch(`/admin/contact-requests/${id}/comments`, {
|
|
698
|
+
method: "POST",
|
|
699
|
+
headers: {
|
|
700
|
+
"Content-Type": "application/json"
|
|
701
|
+
},
|
|
702
|
+
credentials: "include",
|
|
703
|
+
body: JSON.stringify({
|
|
704
|
+
comment: commentText.trim() || void 0,
|
|
705
|
+
images: commentImages.filter((img) => img.trim()).length > 0 ? commentImages.filter((img) => img.trim()) : void 0
|
|
706
|
+
})
|
|
707
|
+
});
|
|
708
|
+
if (!response.ok) {
|
|
709
|
+
const message = await response.text();
|
|
710
|
+
throw new Error(message || "Unable to create comment");
|
|
711
|
+
}
|
|
712
|
+
const detailResponse = await fetch(`/admin/contact-requests/${id}`, {
|
|
713
|
+
credentials: "include"
|
|
714
|
+
});
|
|
715
|
+
if (detailResponse.ok) {
|
|
716
|
+
const detailPayload = await detailResponse.json();
|
|
717
|
+
setComments(detailPayload.comments ?? []);
|
|
718
|
+
}
|
|
719
|
+
setCommentText("");
|
|
720
|
+
setCommentImages([]);
|
|
721
|
+
setCommentSuccess(true);
|
|
722
|
+
setTimeout(() => setCommentSuccess(false), 3e3);
|
|
723
|
+
} catch (commentErr) {
|
|
724
|
+
const message = commentErr instanceof Error ? commentErr.message : "Unable to create comment";
|
|
725
|
+
setCommentError(message);
|
|
726
|
+
} finally {
|
|
727
|
+
setIsCreatingComment(false);
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
const addImageInput = () => {
|
|
731
|
+
setCommentImages([...commentImages, ""]);
|
|
732
|
+
};
|
|
733
|
+
const removeImageInput = (index) => {
|
|
734
|
+
setCommentImages(commentImages.filter((_, i) => i !== index));
|
|
735
|
+
};
|
|
736
|
+
const updateImageUrl = (index, value) => {
|
|
737
|
+
const updated = [...commentImages];
|
|
738
|
+
updated[index] = value;
|
|
739
|
+
setCommentImages(updated);
|
|
740
|
+
};
|
|
489
741
|
if (isLoading) {
|
|
490
742
|
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
743
|
}
|
|
@@ -557,6 +809,103 @@ const ContactRequestDetailPage = () => {
|
|
|
557
809
|
updateError && /* @__PURE__ */ jsx(Text, { size: "small", className: "mt-2 text-ui-fg-error", children: updateError }),
|
|
558
810
|
updateSuccess && /* @__PURE__ */ jsx(Text, { size: "small", className: "mt-2 text-ui-fg-success", children: "Status updated successfully" })
|
|
559
811
|
] }),
|
|
812
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
|
|
813
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", className: "mb-4 text-lg", children: "Assignment" }),
|
|
814
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
815
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
816
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Currently Assigned To" }),
|
|
817
|
+
/* @__PURE__ */ jsx(Text, { className: "font-medium", children: request.assign_to ? /* @__PURE__ */ jsx(Badge, { size: "small", className: "mt-1", children: request.assign_to }) : /* @__PURE__ */ jsx("span", { className: "text-ui-fg-subtle", children: "Unassigned" }) })
|
|
818
|
+
] }),
|
|
819
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-end", children: [
|
|
820
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 relative", children: [
|
|
821
|
+
/* @__PURE__ */ jsx("label", { className: "mb-2 block text-sm font-medium text-ui-fg-base", children: "Assign To Admin" }),
|
|
822
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
823
|
+
/* @__PURE__ */ jsx(
|
|
824
|
+
Input,
|
|
825
|
+
{
|
|
826
|
+
ref: adminInputRef,
|
|
827
|
+
type: "text",
|
|
828
|
+
value: adminSearchQuery,
|
|
829
|
+
onChange: (event) => {
|
|
830
|
+
setAdminSearchQuery(event.target.value);
|
|
831
|
+
setIsAdminDropdownOpen(true);
|
|
832
|
+
setSelectedAdminIndex(-1);
|
|
833
|
+
},
|
|
834
|
+
onFocus: () => {
|
|
835
|
+
setIsAdminDropdownOpen(true);
|
|
836
|
+
if (adminUsers.length === 0) {
|
|
837
|
+
void fetchAdminUsers();
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
onKeyDown: handleAdminKeyDown,
|
|
841
|
+
placeholder: "Search admin by name, email, or ID...",
|
|
842
|
+
className: "w-full"
|
|
843
|
+
}
|
|
844
|
+
),
|
|
845
|
+
isAdminDropdownOpen && /* @__PURE__ */ jsx(
|
|
846
|
+
"div",
|
|
847
|
+
{
|
|
848
|
+
ref: adminDropdownRef,
|
|
849
|
+
className: "absolute z-50 mt-1 w-full max-h-60 overflow-auto rounded-md border border-ui-border-base bg-ui-bg-base shadow-lg",
|
|
850
|
+
children: isLoadingAdmins ? /* @__PURE__ */ jsx("div", { className: "p-4 text-center", children: /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Loading admins..." }) }) : filteredAdmins.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "py-1", children: filteredAdmins.map((admin, index) => /* @__PURE__ */ jsx(
|
|
851
|
+
"li",
|
|
852
|
+
{
|
|
853
|
+
className: `cursor-pointer px-4 py-3 hover:bg-ui-bg-subtle-hover ${index === selectedAdminIndex ? "bg-ui-bg-subtle-hover" : ""}`,
|
|
854
|
+
onClick: () => handleAdminSelect(admin),
|
|
855
|
+
onMouseEnter: () => setSelectedAdminIndex(index),
|
|
856
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
857
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
858
|
+
/* @__PURE__ */ jsx(Text, { className: "font-medium", children: admin.name }),
|
|
859
|
+
admin.id === selectedAssignTo && /* @__PURE__ */ jsx(Badge, { size: "2xsmall", children: "Selected" })
|
|
860
|
+
] }),
|
|
861
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: admin.email }),
|
|
862
|
+
/* @__PURE__ */ jsx(
|
|
863
|
+
Text,
|
|
864
|
+
{
|
|
865
|
+
size: "xsmall",
|
|
866
|
+
className: "text-ui-fg-muted font-mono",
|
|
867
|
+
children: admin.id
|
|
868
|
+
}
|
|
869
|
+
)
|
|
870
|
+
] })
|
|
871
|
+
},
|
|
872
|
+
admin.id
|
|
873
|
+
)) }) : /* @__PURE__ */ jsx("div", { className: "p-4 text-center", children: /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: adminSearchQuery ? "No admins found" : "Start typing to search admins" }) })
|
|
874
|
+
}
|
|
875
|
+
)
|
|
876
|
+
] }),
|
|
877
|
+
selectedAssignTo && /* @__PURE__ */ jsxs(Text, { size: "small", className: "mt-1 text-ui-fg-subtle", children: [
|
|
878
|
+
"Selected Admin ID: ",
|
|
879
|
+
selectedAssignTo
|
|
880
|
+
] })
|
|
881
|
+
] }),
|
|
882
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
883
|
+
/* @__PURE__ */ jsx(
|
|
884
|
+
Button,
|
|
885
|
+
{
|
|
886
|
+
variant: "primary",
|
|
887
|
+
onClick: handleAssign,
|
|
888
|
+
disabled: isAssigning || !selectedAssignTo,
|
|
889
|
+
isLoading: isAssigning,
|
|
890
|
+
children: "Assign"
|
|
891
|
+
}
|
|
892
|
+
),
|
|
893
|
+
request.assign_to && /* @__PURE__ */ jsx(
|
|
894
|
+
Button,
|
|
895
|
+
{
|
|
896
|
+
variant: "secondary",
|
|
897
|
+
onClick: handleUnassign,
|
|
898
|
+
disabled: isAssigning,
|
|
899
|
+
isLoading: isAssigning,
|
|
900
|
+
children: "Unassign"
|
|
901
|
+
}
|
|
902
|
+
)
|
|
903
|
+
] })
|
|
904
|
+
] }),
|
|
905
|
+
assignError && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-error", children: assignError }),
|
|
906
|
+
assignSuccess && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-success", children: "Assignment updated successfully" })
|
|
907
|
+
] })
|
|
908
|
+
] }),
|
|
560
909
|
/* @__PURE__ */ jsxs("div", { className: "grid gap-6 md:grid-cols-2", children: [
|
|
561
910
|
/* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
|
|
562
911
|
/* @__PURE__ */ jsx(Heading, { level: "h2", className: "mb-4 text-lg", children: "Contact Information" }),
|
|
@@ -630,6 +979,103 @@ const ContactRequestDetailPage = () => {
|
|
|
630
979
|
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: key }),
|
|
631
980
|
/* @__PURE__ */ jsx(Text, { size: "small", className: "font-medium", children: typeof value === "string" ? value : JSON.stringify(value) })
|
|
632
981
|
] }, key)) })
|
|
982
|
+
] }),
|
|
983
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-base p-6", children: [
|
|
984
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", className: "mb-4 text-lg", children: "Comments" }),
|
|
985
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-6 space-y-4 rounded-lg border border-ui-border-subtle bg-ui-bg-subtle p-4", children: [
|
|
986
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
987
|
+
/* @__PURE__ */ jsx("label", { className: "mb-2 block text-sm font-medium text-ui-fg-base", children: "Add Comment" }),
|
|
988
|
+
/* @__PURE__ */ jsx(
|
|
989
|
+
Textarea,
|
|
990
|
+
{
|
|
991
|
+
value: commentText,
|
|
992
|
+
onChange: (event) => setCommentText(event.target.value),
|
|
993
|
+
placeholder: "Enter your comment...",
|
|
994
|
+
className: "min-h-[80px]"
|
|
995
|
+
}
|
|
996
|
+
)
|
|
997
|
+
] }),
|
|
998
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
999
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between", children: [
|
|
1000
|
+
/* @__PURE__ */ jsx("label", { className: "text-sm font-medium text-ui-fg-base", children: "Images (URLs)" }),
|
|
1001
|
+
/* @__PURE__ */ jsx(
|
|
1002
|
+
Button,
|
|
1003
|
+
{
|
|
1004
|
+
variant: "transparent",
|
|
1005
|
+
size: "small",
|
|
1006
|
+
onClick: addImageInput,
|
|
1007
|
+
type: "button",
|
|
1008
|
+
children: "Add Image URL"
|
|
1009
|
+
}
|
|
1010
|
+
)
|
|
1011
|
+
] }),
|
|
1012
|
+
commentImages.length > 0 && /* @__PURE__ */ jsx("div", { className: "space-y-2", children: commentImages.map((url, index) => /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
1013
|
+
/* @__PURE__ */ jsx(
|
|
1014
|
+
Input,
|
|
1015
|
+
{
|
|
1016
|
+
type: "text",
|
|
1017
|
+
value: url,
|
|
1018
|
+
onChange: (event) => updateImageUrl(index, event.target.value),
|
|
1019
|
+
placeholder: "https://example.com/image.jpg",
|
|
1020
|
+
className: "flex-1"
|
|
1021
|
+
}
|
|
1022
|
+
),
|
|
1023
|
+
/* @__PURE__ */ jsx(
|
|
1024
|
+
Button,
|
|
1025
|
+
{
|
|
1026
|
+
variant: "transparent",
|
|
1027
|
+
size: "small",
|
|
1028
|
+
onClick: () => removeImageInput(index),
|
|
1029
|
+
type: "button",
|
|
1030
|
+
children: "Remove"
|
|
1031
|
+
}
|
|
1032
|
+
)
|
|
1033
|
+
] }, index)) })
|
|
1034
|
+
] }),
|
|
1035
|
+
/* @__PURE__ */ jsx(
|
|
1036
|
+
Button,
|
|
1037
|
+
{
|
|
1038
|
+
variant: "primary",
|
|
1039
|
+
onClick: handleCreateComment,
|
|
1040
|
+
disabled: isCreatingComment || !commentText.trim() && commentImages.filter((img) => img.trim()).length === 0,
|
|
1041
|
+
isLoading: isCreatingComment,
|
|
1042
|
+
children: "Add Comment"
|
|
1043
|
+
}
|
|
1044
|
+
),
|
|
1045
|
+
commentError && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-error", children: commentError }),
|
|
1046
|
+
commentSuccess && /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-success", children: "Comment added successfully" })
|
|
1047
|
+
] }),
|
|
1048
|
+
comments.length > 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-4", children: comments.map((comment) => /* @__PURE__ */ jsxs(
|
|
1049
|
+
"div",
|
|
1050
|
+
{
|
|
1051
|
+
className: "rounded-lg border border-ui-border-subtle bg-ui-bg-subtle p-4",
|
|
1052
|
+
children: [
|
|
1053
|
+
/* @__PURE__ */ jsx("div", { className: "mb-2 flex items-center justify-between", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1054
|
+
/* @__PURE__ */ jsx(Badge, { size: "2xsmall", children: comment.admin_id }),
|
|
1055
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: new Date(comment.created_at).toLocaleString() })
|
|
1056
|
+
] }) }),
|
|
1057
|
+
comment.comment && /* @__PURE__ */ jsx(Text, { className: "mb-2 whitespace-pre-wrap", children: comment.comment }),
|
|
1058
|
+
comment.images && comment.images.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-3 space-y-2", children: [
|
|
1059
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Images:" }),
|
|
1060
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: comment.images.map((imageUrl, index) => /* @__PURE__ */ jsxs(
|
|
1061
|
+
"a",
|
|
1062
|
+
{
|
|
1063
|
+
href: imageUrl,
|
|
1064
|
+
target: "_blank",
|
|
1065
|
+
rel: "noopener noreferrer",
|
|
1066
|
+
className: "text-sm text-ui-fg-interactive hover:underline",
|
|
1067
|
+
children: [
|
|
1068
|
+
"Image ",
|
|
1069
|
+
index + 1
|
|
1070
|
+
]
|
|
1071
|
+
},
|
|
1072
|
+
index
|
|
1073
|
+
)) })
|
|
1074
|
+
] })
|
|
1075
|
+
]
|
|
1076
|
+
},
|
|
1077
|
+
comment.id
|
|
1078
|
+
)) }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "No comments yet" })
|
|
633
1079
|
] })
|
|
634
1080
|
] }) });
|
|
635
1081
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = void 0;
|
|
4
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const contact_requests_1 = require("../../../../../modules/contact-requests");
|
|
7
|
+
const validators_1 = require("../../validators");
|
|
8
|
+
const POST = async (req, res) => {
|
|
9
|
+
const { id } = req.params;
|
|
10
|
+
try {
|
|
11
|
+
// Validate input
|
|
12
|
+
const body = validators_1.AdminAssignContactRequestSchema.parse(req.body ?? {});
|
|
13
|
+
const service = req.scope.resolve(contact_requests_1.CONTACT_REQUEST_MODULE);
|
|
14
|
+
const request = await service.assignRequest({
|
|
15
|
+
id,
|
|
16
|
+
assign_to: body.assign_to,
|
|
17
|
+
});
|
|
18
|
+
res.status(200).json({ request });
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
// Handle validation errors
|
|
22
|
+
if (error instanceof zod_1.z.ZodError) {
|
|
23
|
+
res.status(400).json({
|
|
24
|
+
message: "Invalid request data",
|
|
25
|
+
errors: error.errors,
|
|
26
|
+
type: "invalid_data",
|
|
27
|
+
code: "invalid_data",
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Handle MedusaError
|
|
32
|
+
if (error instanceof utils_1.MedusaError) {
|
|
33
|
+
const statusCode = error.type === utils_1.MedusaError.Types.NOT_FOUND ? 404 : 400;
|
|
34
|
+
res.status(statusCode).json({
|
|
35
|
+
message: error.message,
|
|
36
|
+
type: error.type,
|
|
37
|
+
code: error.type,
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Handle unexpected errors
|
|
42
|
+
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
|
|
43
|
+
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
44
|
+
console.error("Error assigning contact request:", error);
|
|
45
|
+
res.status(500).json({
|
|
46
|
+
message: errorMessage,
|
|
47
|
+
type: "unknown_error",
|
|
48
|
+
code: "unknown_error",
|
|
49
|
+
...(process.env.NODE_ENV === "development" && { stack: errorStack }),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
exports.POST = POST;
|
|
54
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL2NvbnRhY3QtcmVxdWVzdHMvW2lkXS9hc3NpZ24vcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQ0EscURBQXVEO0FBQ3ZELDZCQUF1QjtBQUN2Qiw4RUFBZ0Y7QUFFaEYsaURBQWtFO0FBRTNELE1BQU0sSUFBSSxHQUFHLEtBQUssRUFBRSxHQUFrQixFQUFFLEdBQW1CLEVBQUUsRUFBRTtJQUNwRSxNQUFNLEVBQUUsRUFBRSxFQUFFLEdBQUcsR0FBRyxDQUFDLE1BQU0sQ0FBQTtJQUV6QixJQUFJLENBQUM7UUFDSCxpQkFBaUI7UUFDakIsTUFBTSxJQUFJLEdBQUcsNENBQStCLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxJQUFJLElBQUksRUFBRSxDQUFDLENBQUE7UUFFbEUsTUFBTSxPQUFPLEdBQUcsR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQy9CLHlDQUFzQixDQUN2QixDQUFBO1FBRUQsTUFBTSxPQUFPLEdBQUcsTUFBTSxPQUFPLENBQUMsYUFBYSxDQUFDO1lBQzFDLEVBQUU7WUFDRixTQUFTLEVBQUUsSUFBSSxDQUFDLFNBQVM7U0FDMUIsQ0FBQyxDQUFBO1FBRUYsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUMsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFBO0lBQ25DLENBQUM7SUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1FBQ2YsMkJBQTJCO1FBQzNCLElBQUksS0FBSyxZQUFZLE9BQUMsQ0FBQyxRQUFRLEVBQUUsQ0FBQztZQUNoQyxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQztnQkFDbkIsT0FBTyxFQUFFLHNCQUFzQjtnQkFDL0IsTUFBTSxFQUFFLEtBQUssQ0FBQyxNQUFNO2dCQUNwQixJQUFJLEVBQUUsY0FBYztnQkFDcEIsSUFBSSxFQUFFLGNBQWM7YUFDckIsQ0FBQyxDQUFBO1lBQ0YsT0FBTTtRQUNSLENBQUM7UUFFRCxxQkFBcUI7UUFDckIsSUFBSSxLQUFLLFlBQVksbUJBQVcsRUFBRSxDQUFDO1lBQ2pDLE1BQU0sVUFBVSxHQUNkLEtBQUssQ0FBQyxJQUFJLEtBQUssbUJBQVcsQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQTtZQUN4RCxHQUFHLENBQUMsTUFBTSxDQUFDLFVBQVUsQ0FBQyxDQUFDLElBQUksQ0FBQztnQkFDMUIsT0FBTyxFQUFFLEtBQUssQ0FBQyxPQUFPO2dCQUN0QixJQUFJLEVBQUUsS0FBSyxDQUFDLElBQUk7Z0JBQ2hCLElBQUksRUFBRSxLQUFLLENBQUMsSUFBSTthQUNqQixDQUFDLENBQUE7WUFDRixPQUFNO1FBQ1IsQ0FBQztRQUVELDJCQUEyQjtRQUMzQixNQUFNLFlBQVksR0FDaEIsS0FBSyxZQUFZLEtBQUssQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsMkJBQTJCLENBQUE7UUFDdEUsTUFBTSxVQUFVLEdBQUcsS0FBSyxZQUFZLEtBQUssQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFBO1FBRW5FLE9BQU8sQ0FBQyxLQUFLLENBQUMsa0NBQWtDLEVBQUUsS0FBSyxDQUFDLENBQUE7UUFFeEQsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUM7WUFDbkIsT0FBTyxFQUFFLFlBQVk7WUFDckIsSUFBSSxFQUFFLGVBQWU7WUFDckIsSUFBSSxFQUFFLGVBQWU7WUFDckIsR0FBRyxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsUUFBUSxLQUFLLGFBQWEsSUFBSSxFQUFFLEtBQUssRUFBRSxVQUFVLEVBQUUsQ0FBQztTQUNyRSxDQUFDLENBQUE7SUFDSixDQUFDO0FBQ0gsQ0FBQyxDQUFBO0FBdkRZLFFBQUEsSUFBSSxRQXVEaEIifQ==
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = exports.GET = void 0;
|
|
4
|
+
const contact_requests_1 = require("../../../../../modules/contact-requests");
|
|
5
|
+
const validators_1 = require("../../validators");
|
|
6
|
+
const GET = async (req, res) => {
|
|
7
|
+
const { id } = req.params;
|
|
8
|
+
const service = req.scope.resolve(contact_requests_1.CONTACT_REQUEST_MODULE);
|
|
9
|
+
const comments = await service.listComments(id);
|
|
10
|
+
res.status(200).json({ comments });
|
|
11
|
+
};
|
|
12
|
+
exports.GET = GET;
|
|
13
|
+
const POST = async (req, res) => {
|
|
14
|
+
const { id } = req.params;
|
|
15
|
+
const body = validators_1.AdminCreateCommentSchema.parse(req.body ?? {});
|
|
16
|
+
const authContext = req.auth_context;
|
|
17
|
+
const adminId = authContext?.actor_id || authContext?.user_id || "admin";
|
|
18
|
+
const service = req.scope.resolve(contact_requests_1.CONTACT_REQUEST_MODULE);
|
|
19
|
+
const comment = await service.createComment({
|
|
20
|
+
contact_request_id: id,
|
|
21
|
+
admin_id: adminId,
|
|
22
|
+
comment: body.comment,
|
|
23
|
+
images: body.images,
|
|
24
|
+
});
|
|
25
|
+
res.status(201).json({ comment });
|
|
26
|
+
};
|
|
27
|
+
exports.POST = POST;
|
|
28
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL2NvbnRhY3QtcmVxdWVzdHMvW2lkXS9jb21tZW50cy9yb3V0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFDQSw4RUFBZ0Y7QUFFaEYsaURBQTJEO0FBRXBELE1BQU0sR0FBRyxHQUFHLEtBQUssRUFBRSxHQUFrQixFQUFFLEdBQW1CLEVBQUUsRUFBRTtJQUNuRSxNQUFNLEVBQUUsRUFBRSxFQUFFLEdBQUcsR0FBRyxDQUFDLE1BQU0sQ0FBQTtJQUV6QixNQUFNLE9BQU8sR0FBRyxHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FDL0IseUNBQXNCLENBQ3ZCLENBQUE7SUFFRCxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxZQUFZLENBQUMsRUFBRSxDQUFDLENBQUE7SUFFL0MsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUMsRUFBRSxRQUFRLEVBQUUsQ0FBQyxDQUFBO0FBQ3BDLENBQUMsQ0FBQTtBQVZZLFFBQUEsR0FBRyxPQVVmO0FBRU0sTUFBTSxJQUFJLEdBQUcsS0FBSyxFQUFFLEdBQWtCLEVBQUUsR0FBbUIsRUFBRSxFQUFFO0lBQ3BFLE1BQU0sRUFBRSxFQUFFLEVBQUUsR0FBRyxHQUFHLENBQUMsTUFBTSxDQUFBO0lBQ3pCLE1BQU0sSUFBSSxHQUFHLHFDQUF3QixDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsSUFBSSxJQUFJLEVBQUUsQ0FBQyxDQUFBO0lBRTNELE1BQU0sV0FBVyxHQUFJLEdBRW5CLENBQUMsWUFBWSxDQUFBO0lBQ2YsTUFBTSxPQUFPLEdBQUcsV0FBVyxFQUFFLFFBQVEsSUFBSSxXQUFXLEVBQUUsT0FBTyxJQUFJLE9BQU8sQ0FBQTtJQUV4RSxNQUFNLE9BQU8sR0FBRyxHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FDL0IseUNBQXNCLENBQ3ZCLENBQUE7SUFFRCxNQUFNLE9BQU8sR0FBRyxNQUFNLE9BQU8sQ0FBQyxhQUFhLENBQUM7UUFDMUMsa0JBQWtCLEVBQUUsRUFBRTtRQUN0QixRQUFRLEVBQUUsT0FBTztRQUNqQixPQUFPLEVBQUUsSUFBSSxDQUFDLE9BQU87UUFDckIsTUFBTSxFQUFFLElBQUksQ0FBQyxNQUFNO0tBQ3BCLENBQUMsQ0FBQTtJQUVGLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxDQUFDLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQTtBQUNuQyxDQUFDLENBQUE7QUFyQlksUUFBQSxJQUFJLFFBcUJoQiJ9
|