medusa-contact-us 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/.medusa/server/src/admin/index.js +523 -0
  2. package/.medusa/server/src/admin/index.mjs +524 -0
  3. package/.medusa/server/src/api/admin/contact-requests/[id]/comments/route.js +22 -0
  4. package/.medusa/server/src/api/admin/contact-requests/[id]/route.js +41 -0
  5. package/.medusa/server/src/api/admin/contact-requests/route.js +31 -0
  6. package/.medusa/server/src/api/admin/contact-requests/validators.js +19 -0
  7. package/.medusa/server/src/api/admin/plugin/route.js +11 -0
  8. package/.medusa/server/src/api/store/contact-requests/route.js +29 -0
  9. package/.medusa/server/src/api/store/contact-requests/validators.js +15 -0
  10. package/.medusa/server/src/api/store/plugin/route.js +11 -0
  11. package/.medusa/server/src/constants.js +5 -0
  12. package/.medusa/server/src/helpers/__tests__/submit-contact-request.test.js +93 -0
  13. package/.medusa/server/src/helpers/index.js +6 -0
  14. package/.medusa/server/src/helpers/submit-contact-request.js +66 -0
  15. package/.medusa/server/src/index.js +30 -0
  16. package/.medusa/server/src/modules/contact-requests/index.js +15 -0
  17. package/.medusa/server/src/modules/contact-requests/migrations/Migration20241124090000.js +46 -0
  18. package/.medusa/server/src/modules/contact-requests/models/contact-request-comment.js +12 -0
  19. package/.medusa/server/src/modules/contact-requests/models/contact-request.js +16 -0
  20. package/.medusa/server/src/modules/contact-requests/service.js +186 -0
  21. package/.medusa/server/src/plugin-options.js +16 -0
  22. package/.medusa/server/src/types/__tests__/contact-options.test.js +83 -0
  23. package/.medusa/server/src/types.js +210 -0
  24. package/.medusa/server/src/utils/__tests__/payload-validator.test.js +81 -0
  25. package/.medusa/server/src/utils/payload.js +127 -0
  26. package/.medusa/server/src/workflows/create-contact-request.js +30 -0
  27. package/.medusa/server/src/workflows/index.js +8 -0
  28. package/.medusa/server/src/workflows/steps/create-contact-request-step.js +17 -0
  29. package/.medusa/server/src/workflows/steps/send-contact-notification-step.js +48 -0
  30. package/.medusa/server/src/workflows/steps/update-contact-request-status-step.js +17 -0
  31. package/.medusa/server/src/workflows/update-contact-request-status.js +32 -0
  32. package/README.md +213 -0
  33. package/package.json +72 -0
@@ -0,0 +1,524 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useState, useCallback, useEffect, useMemo } from "react";
3
+ import { Link, useParams, useNavigate } from "react-router-dom";
4
+ import { defineRouteConfig } from "@medusajs/admin-sdk";
5
+ import { Container, Heading, Text, Button, Input, Badge, toast, Textarea } from "@medusajs/ui";
6
+ import { Envelope, ArrowUturnLeft } from "@medusajs/icons";
7
+ const useDebounce = (value, delay) => {
8
+ const [debouncedValue, setDebouncedValue] = useState(value);
9
+ useEffect(() => {
10
+ const handler = setTimeout(() => setDebouncedValue(value), delay);
11
+ return () => clearTimeout(handler);
12
+ }, [value, delay]);
13
+ return debouncedValue;
14
+ };
15
+ const statusBadgeClass = (status) => {
16
+ if (status.includes("close") || status.includes("resolved")) {
17
+ return "bg-ui-tag-green-bg text-ui-tag-green-text";
18
+ }
19
+ if (status.includes("review") || status.includes("progress")) {
20
+ return "bg-ui-tag-orange-bg text-ui-tag-orange-text";
21
+ }
22
+ return "bg-ui-tag-neutral-bg text-ui-tag-neutral-text";
23
+ };
24
+ const ContactRequestsPage = () => {
25
+ const [requests, setRequests] = useState([]);
26
+ const [statuses, setStatuses] = useState([]);
27
+ const [statusFilter, setStatusFilter] = useState("all");
28
+ const [query, setQuery] = useState("");
29
+ const debouncedQuery = useDebounce(query, 300);
30
+ const [isLoading, setIsLoading] = useState(true);
31
+ const [isFetchingMore, setIsFetchingMore] = useState(false);
32
+ const [error, setError] = useState(null);
33
+ const [offset, setOffset] = useState(0);
34
+ const [count, setCount] = useState(0);
35
+ const limit = 20;
36
+ const loadRequests = useCallback(
37
+ async (nextOffset, replace = false) => {
38
+ var _a;
39
+ try {
40
+ if (replace) {
41
+ setIsLoading(true);
42
+ } else {
43
+ setIsFetchingMore(true);
44
+ }
45
+ setError(null);
46
+ const params = new URLSearchParams();
47
+ params.set("limit", String(limit));
48
+ params.set("offset", String(nextOffset));
49
+ if (statusFilter !== "all") {
50
+ params.set("status", statusFilter);
51
+ }
52
+ if (debouncedQuery.trim()) {
53
+ params.set("q", debouncedQuery.trim());
54
+ }
55
+ const response = await fetch(
56
+ `/admin/contact-requests?${params.toString()}`,
57
+ { credentials: "include" }
58
+ );
59
+ if (!response.ok) {
60
+ const message = await response.text();
61
+ throw new Error(message || "Unable to load contact requests");
62
+ }
63
+ const payload = await response.json();
64
+ setStatuses(payload.statuses ?? []);
65
+ setCount(payload.count ?? 0);
66
+ setOffset(nextOffset + (((_a = payload.contact_requests) == null ? void 0 : _a.length) ?? 0));
67
+ setRequests(
68
+ (prev) => replace ? payload.contact_requests ?? [] : [...prev, ...payload.contact_requests ?? []]
69
+ );
70
+ } catch (loadError) {
71
+ const message = loadError instanceof Error ? loadError.message : "Unable to load contact requests";
72
+ setError(message);
73
+ } finally {
74
+ setIsLoading(false);
75
+ setIsFetchingMore(false);
76
+ }
77
+ },
78
+ [statusFilter, debouncedQuery]
79
+ );
80
+ useEffect(() => {
81
+ void loadRequests(0, true);
82
+ }, [statusFilter, debouncedQuery, loadRequests]);
83
+ const hasMore = useMemo(() => offset < count, [offset, count]);
84
+ const displayedStatuses = useMemo(
85
+ () => [{ code: "all", label: "All statuses" }, ...statuses],
86
+ [statuses]
87
+ );
88
+ return /* @__PURE__ */ jsx("div", { className: "w-full p-6", children: /* @__PURE__ */ jsxs(Container, { className: "mx-auto flex w-full max-w-6xl flex-col gap-6 p-6", children: [
89
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between", children: [
90
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
91
+ /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Contact Requests" }),
92
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Monitor inbound storefront messages and keep track of their statuses without leaving the admin." })
93
+ ] }),
94
+ /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: () => loadRequests(0, true), children: "Refresh" })
95
+ ] }),
96
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
97
+ /* @__PURE__ */ jsx(
98
+ Input,
99
+ {
100
+ placeholder: "Search by email",
101
+ value: query,
102
+ onChange: (event) => setQuery(event.target.value),
103
+ className: "md:max-w-sm"
104
+ }
105
+ ),
106
+ /* @__PURE__ */ jsx(
107
+ "select",
108
+ {
109
+ value: statusFilter,
110
+ onChange: (event) => setStatusFilter(event.target.value),
111
+ 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",
112
+ children: displayedStatuses.map((status) => /* @__PURE__ */ jsx("option", { value: status.code, children: status.label }, status.code))
113
+ }
114
+ )
115
+ ] }),
116
+ error ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-ui-border-strong p-6 text-center", children: [
117
+ /* @__PURE__ */ jsx(Text, { weight: "plus", className: "text-ui-fg-error", children: error }),
118
+ /* @__PURE__ */ jsx("div", { className: "mt-4 flex justify-center", children: /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => loadRequests(0, true), children: "Try again" }) })
119
+ ] }) : null,
120
+ isLoading ? /* @__PURE__ */ jsx("div", { className: "flex justify-center py-16", children: /* @__PURE__ */ jsx(Text, { children: "Loading contact requests..." }) }) : requests.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-dashed border-ui-border-strong p-10 text-center", children: [
121
+ /* @__PURE__ */ jsx(Heading, { level: "h3", className: "text-xl", children: "No contact requests yet" }),
122
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "mt-2 text-ui-fg-subtle", children: "The contact form is ready. Share it with your customers and new requests will show up here." })
123
+ ] }) : /* @__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: [
124
+ /* @__PURE__ */ jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxs("tr", { children: [
125
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Email" }),
126
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Status" }),
127
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Created" }),
128
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-3 text-right text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Details" })
129
+ ] }) }),
130
+ /* @__PURE__ */ jsx("tbody", { className: "divide-y divide-ui-border-subtle", children: requests.map((request) => {
131
+ const statusOption = statuses.find(
132
+ (status) => status.code === request.status
133
+ );
134
+ return /* @__PURE__ */ jsxs("tr", { className: "hover:bg-ui-bg-subtle/60", children: [
135
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-4 font-medium text-ui-fg-base", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
136
+ /* @__PURE__ */ jsx("span", { children: request.email }),
137
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: request.id })
138
+ ] }) }),
139
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-4", children: /* @__PURE__ */ jsx(
140
+ Badge,
141
+ {
142
+ size: "2xsmall",
143
+ className: `uppercase ${statusBadgeClass(
144
+ request.status
145
+ )}`,
146
+ children: (statusOption == null ? void 0 : statusOption.label) ?? request.status
147
+ }
148
+ ) }),
149
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-4 text-ui-fg-subtle", children: new Date(request.created_at).toLocaleString() }),
150
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-4 text-right", children: /* @__PURE__ */ jsx(
151
+ Link,
152
+ {
153
+ to: `/contact-requests/${request.id}`,
154
+ className: "text-ui-fg-interactive underline",
155
+ children: "View"
156
+ }
157
+ ) })
158
+ ] }, request.id);
159
+ }) })
160
+ ] }) }),
161
+ hasMore ? /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(
162
+ Button,
163
+ {
164
+ variant: "secondary",
165
+ isLoading: isFetchingMore,
166
+ onClick: () => loadRequests(offset, false),
167
+ children: "Load more"
168
+ }
169
+ ) }) : null
170
+ ] }) });
171
+ };
172
+ const config = defineRouteConfig({
173
+ label: "Contact Requests",
174
+ icon: Envelope
175
+ });
176
+ const formatDateTime = (value) => new Date(value).toLocaleString(void 0, {
177
+ dateStyle: "medium",
178
+ timeStyle: "short"
179
+ });
180
+ const ContactRequestDetailPage = () => {
181
+ const { id } = useParams();
182
+ const navigate = useNavigate();
183
+ const [request, setRequest] = useState(null);
184
+ const [comments, setComments] = useState([]);
185
+ const [statusOptions, setStatusOptions] = useState([]);
186
+ const [transitions, setTransitions] = useState([]);
187
+ const [statusSelection, setStatusSelection] = useState("");
188
+ const [statusNote, setStatusNote] = useState("");
189
+ const [newComment, setNewComment] = useState("");
190
+ const [isLoading, setIsLoading] = useState(true);
191
+ const [error, setError] = useState(null);
192
+ const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
193
+ const [isSavingComment, setIsSavingComment] = useState(false);
194
+ const loadDetail = useCallback(async () => {
195
+ if (!id) {
196
+ return;
197
+ }
198
+ setIsLoading(true);
199
+ setError(null);
200
+ try {
201
+ const response = await fetch(`/admin/contact-requests/${id}`, {
202
+ credentials: "include"
203
+ });
204
+ if (!response.ok) {
205
+ const message = await response.text();
206
+ throw new Error(message || "Unable to load contact request");
207
+ }
208
+ const payload = await response.json();
209
+ setRequest(payload.contact_request);
210
+ setComments(payload.comments ?? []);
211
+ setStatusOptions(payload.statuses ?? []);
212
+ setTransitions(payload.transitions ?? []);
213
+ } catch (loadError) {
214
+ const message = loadError instanceof Error ? loadError.message : "Unable to load contact request";
215
+ setError(message);
216
+ } finally {
217
+ setIsLoading(false);
218
+ }
219
+ }, [id]);
220
+ useEffect(() => {
221
+ void loadDetail();
222
+ }, [loadDetail]);
223
+ useEffect(() => {
224
+ if (transitions.length) {
225
+ setStatusSelection(transitions[0]);
226
+ } else {
227
+ setStatusSelection("");
228
+ }
229
+ }, [transitions]);
230
+ const statusLabelMap = useMemo(() => {
231
+ const map = /* @__PURE__ */ new Map();
232
+ statusOptions.forEach((status) => map.set(status.code, status.label));
233
+ return map;
234
+ }, [statusOptions]);
235
+ const orderedHistory = useMemo(() => {
236
+ return [...(request == null ? void 0 : request.status_history) ?? []].sort(
237
+ (a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
238
+ );
239
+ }, [request == null ? void 0 : request.status_history]);
240
+ const handleStatusUpdate = useCallback(async () => {
241
+ if (!id || !statusSelection) {
242
+ toast.error("Select a status to continue");
243
+ return;
244
+ }
245
+ setIsUpdatingStatus(true);
246
+ try {
247
+ const response = await fetch(`/admin/contact-requests/${id}`, {
248
+ method: "PATCH",
249
+ credentials: "include",
250
+ headers: {
251
+ "Content-Type": "application/json"
252
+ },
253
+ body: JSON.stringify({
254
+ status: statusSelection,
255
+ note: statusNote || void 0
256
+ })
257
+ });
258
+ if (!response.ok) {
259
+ const message = await response.text();
260
+ throw new Error(message || "Unable to update status");
261
+ }
262
+ const payload = await response.json();
263
+ setRequest(payload.contact_request);
264
+ setTransitions(payload.transitions ?? []);
265
+ setStatusNote("");
266
+ toast.success("Status updated");
267
+ } catch (updateError) {
268
+ const message = updateError instanceof Error ? updateError.message : "Unable to update status";
269
+ toast.error(message);
270
+ } finally {
271
+ setIsUpdatingStatus(false);
272
+ }
273
+ }, [id, statusSelection, statusNote]);
274
+ const handleAddComment = useCallback(async () => {
275
+ if (!id) {
276
+ return;
277
+ }
278
+ if (!newComment.trim()) {
279
+ toast.error("Comment cannot be empty");
280
+ return;
281
+ }
282
+ setIsSavingComment(true);
283
+ try {
284
+ const response = await fetch(
285
+ `/admin/contact-requests/${id}/comments`,
286
+ {
287
+ method: "POST",
288
+ credentials: "include",
289
+ headers: {
290
+ "Content-Type": "application/json"
291
+ },
292
+ body: JSON.stringify({ comment: newComment })
293
+ }
294
+ );
295
+ if (!response.ok) {
296
+ const message = await response.text();
297
+ throw new Error(message || "Unable to add comment");
298
+ }
299
+ const payload = await response.json();
300
+ setComments((prev) => [payload.comment, ...prev]);
301
+ setNewComment("");
302
+ toast.success("Comment added");
303
+ } catch (commentError) {
304
+ const message = commentError instanceof Error ? commentError.message : "Unable to add comment";
305
+ toast.error(message);
306
+ } finally {
307
+ setIsSavingComment(false);
308
+ }
309
+ }, [id, newComment]);
310
+ if (isLoading) {
311
+ return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-32", children: /* @__PURE__ */ jsx(Text, { children: "Loading contact request..." }) });
312
+ }
313
+ if (error || !request) {
314
+ return /* @__PURE__ */ jsxs(Container, { className: "mx-auto mt-12 max-w-3xl p-8 text-center", children: [
315
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Unable to load request" }),
316
+ /* @__PURE__ */ jsx(Text, { className: "mt-2 text-ui-fg-subtle", children: error ?? "Unknown error" }),
317
+ /* @__PURE__ */ jsx(
318
+ Button,
319
+ {
320
+ variant: "secondary",
321
+ className: "mt-6",
322
+ onClick: () => navigate("/contact-requests"),
323
+ children: "Back to list"
324
+ }
325
+ )
326
+ ] });
327
+ }
328
+ 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: [
329
+ /* @__PURE__ */ jsxs(
330
+ "button",
331
+ {
332
+ className: "flex w-fit items-center gap-2 text-ui-fg-interactive",
333
+ onClick: () => navigate("/contact-requests"),
334
+ children: [
335
+ /* @__PURE__ */ jsx(ArrowUturnLeft, { className: "h-4 w-4" }),
336
+ "Back to requests"
337
+ ]
338
+ }
339
+ ),
340
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-3", children: [
341
+ /* @__PURE__ */ jsx(Heading, { level: "h1", children: request.email }),
342
+ /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
343
+ "Request ID ",
344
+ request.id
345
+ ] })
346
+ ] }),
347
+ /* @__PURE__ */ jsxs("section", { className: "grid gap-6 lg:grid-cols-3", children: [
348
+ /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-ui-border-base p-5 lg:col-span-2", children: [
349
+ /* @__PURE__ */ jsx(Heading, { level: "h3", className: "text-lg", children: "Submitted fields" }),
350
+ /* @__PURE__ */ jsx("dl", { className: "mt-4 grid gap-4", children: Object.entries(request.payload ?? {}).length === 0 ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "No additional fields captured for this request." }) : Object.entries(request.payload ?? {}).map(([key, value]) => /* @__PURE__ */ jsxs(
351
+ "div",
352
+ {
353
+ className: "flex flex-col gap-1 rounded-lg bg-ui-bg-subtle p-3",
354
+ children: [
355
+ /* @__PURE__ */ jsx(Text, { className: "text-xs uppercase text-ui-fg-muted", children: key }),
356
+ /* @__PURE__ */ jsx(Text, { className: "font-medium text-ui-fg-base", children: typeof value === "object" ? JSON.stringify(value, null, 2) : String(value) })
357
+ ]
358
+ },
359
+ key
360
+ )) })
361
+ ] }),
362
+ /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-ui-border-base p-5", children: [
363
+ /* @__PURE__ */ jsx(Heading, { level: "h3", className: "text-lg", children: "Status" }),
364
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex flex-col gap-3", children: [
365
+ /* @__PURE__ */ jsx(Badge, { className: "w-fit bg-ui-tag-neutral-bg text-ui-tag-neutral-text", children: statusLabelMap.get(request.status) ?? request.status }),
366
+ /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
367
+ "Created ",
368
+ formatDateTime(request.created_at)
369
+ ] }),
370
+ /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
371
+ "Last updated ",
372
+ formatDateTime(request.updated_at)
373
+ ] })
374
+ ] }),
375
+ /* @__PURE__ */ jsxs("div", { className: "mt-6 flex flex-col gap-3", children: [
376
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-semibold uppercase tracking-wide text-ui-fg-muted", children: "Next status" }),
377
+ /* @__PURE__ */ jsxs(
378
+ "select",
379
+ {
380
+ value: statusSelection,
381
+ onChange: (event) => setStatusSelection(event.target.value),
382
+ 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",
383
+ disabled: !transitions.length,
384
+ children: [
385
+ /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: transitions.length ? "Select status" : "No further transitions" }),
386
+ transitions.map((status) => /* @__PURE__ */ jsx("option", { value: status, children: statusLabelMap.get(status) ?? status }, status))
387
+ ]
388
+ }
389
+ ),
390
+ /* @__PURE__ */ jsx(
391
+ Textarea,
392
+ {
393
+ rows: 3,
394
+ value: statusNote,
395
+ onChange: (event) => setStatusNote(event.target.value),
396
+ placeholder: "Optional note for history"
397
+ }
398
+ ),
399
+ /* @__PURE__ */ jsx(
400
+ Button,
401
+ {
402
+ onClick: () => void handleStatusUpdate(),
403
+ disabled: !statusSelection,
404
+ isLoading: isUpdatingStatus,
405
+ children: "Update status"
406
+ }
407
+ )
408
+ ] })
409
+ ] })
410
+ ] }),
411
+ /* @__PURE__ */ jsxs("section", { className: "grid gap-6 lg:grid-cols-3", children: [
412
+ /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-ui-border-base p-5 lg:col-span-2", children: [
413
+ /* @__PURE__ */ jsx(Heading, { level: "h3", className: "text-lg", children: "Status history" }),
414
+ /* @__PURE__ */ jsx("div", { className: "mt-4 flex flex-col gap-4", children: orderedHistory.map((entry) => /* @__PURE__ */ jsxs(
415
+ "div",
416
+ {
417
+ className: "rounded-lg border border-ui-border-subtle p-3",
418
+ children: [
419
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
420
+ /* @__PURE__ */ jsx(Text, { weight: "plus", children: statusLabelMap.get(entry.status) ?? entry.status }),
421
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: formatDateTime(entry.updated_at) })
422
+ ] }),
423
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: entry.updated_by ? `By ${entry.updated_by}` : "System" }),
424
+ entry.note ? /* @__PURE__ */ jsx(Text, { size: "small", className: "mt-2", children: entry.note }) : null
425
+ ]
426
+ },
427
+ `${entry.status}-${entry.updated_at}`
428
+ )) })
429
+ ] }),
430
+ /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-ui-border-base p-5", children: [
431
+ /* @__PURE__ */ jsx(Heading, { level: "h3", className: "text-lg", children: "Admin comments" }),
432
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex flex-col gap-3", children: [
433
+ /* @__PURE__ */ jsx(
434
+ Textarea,
435
+ {
436
+ rows: 4,
437
+ placeholder: "Leave an internal note",
438
+ value: newComment,
439
+ onChange: (event) => setNewComment(event.target.value)
440
+ }
441
+ ),
442
+ /* @__PURE__ */ jsx(
443
+ Button,
444
+ {
445
+ variant: "secondary",
446
+ onClick: () => void handleAddComment(),
447
+ isLoading: isSavingComment,
448
+ children: "Add comment"
449
+ }
450
+ )
451
+ ] }),
452
+ /* @__PURE__ */ jsx("div", { className: "mt-6 flex flex-col gap-4", children: comments.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "No comments yet." }) : comments.map((comment) => /* @__PURE__ */ jsxs(
453
+ "div",
454
+ {
455
+ className: "rounded-lg bg-ui-bg-subtle p-3",
456
+ children: [
457
+ /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
458
+ comment.actor_id ?? "Admin",
459
+ " •",
460
+ " ",
461
+ formatDateTime(comment.created_at)
462
+ ] }),
463
+ /* @__PURE__ */ jsx(Text, { className: "mt-2", children: comment.comment })
464
+ ]
465
+ },
466
+ comment.id
467
+ )) })
468
+ ] })
469
+ ] })
470
+ ] }) });
471
+ };
472
+ defineRouteConfig({
473
+ parentRoute: "/contact-requests"
474
+ });
475
+ const en = {
476
+ "contact-requests.title": "Contact Requests",
477
+ "contact-requests.subtitle": "Monitor submissions and keep track of every response.",
478
+ "contact-requests.empty": "No contact requests yet.",
479
+ "contact-requests.detail.status": "Status",
480
+ "contact-requests.detail.history": "Status history",
481
+ "contact-requests.detail.comments": "Admin comments"
482
+ };
483
+ const i18nTranslations0 = {
484
+ en
485
+ };
486
+ const widgetModule = { widgets: [] };
487
+ const routeModule = {
488
+ routes: [
489
+ {
490
+ Component: ContactRequestsPage,
491
+ path: "/contact-requests"
492
+ },
493
+ {
494
+ Component: ContactRequestDetailPage,
495
+ path: "/contact-requests/:id"
496
+ }
497
+ ]
498
+ };
499
+ const menuItemModule = {
500
+ menuItems: [
501
+ {
502
+ label: config.label,
503
+ icon: config.icon,
504
+ path: "/contact-requests",
505
+ nested: void 0
506
+ }
507
+ ]
508
+ };
509
+ const formModule = { customFields: {} };
510
+ const displayModule = {
511
+ displays: {}
512
+ };
513
+ const i18nModule = { resources: i18nTranslations0 };
514
+ const plugin = {
515
+ widgetModule,
516
+ routeModule,
517
+ menuItemModule,
518
+ formModule,
519
+ displayModule,
520
+ i18nModule
521
+ };
522
+ export {
523
+ plugin as default
524
+ };
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.POST = void 0;
4
+ const constants_1 = require("../../../../../constants");
5
+ const validators_1 = require("../../validators");
6
+ const POST = async (req, res) => {
7
+ const body = validators_1.AdminAddCommentSchema.parse(req.body ?? {});
8
+ const service = req.scope.resolve(constants_1.CONTACT_REQUEST_MODULE);
9
+ const actorId = req.auth_context?.actor_id ||
10
+ req.auth_context?.user_id ||
11
+ "admin";
12
+ const comment = await service.addComment({
13
+ request_id: req.params.id,
14
+ comment: body.comment,
15
+ actor_id: actorId,
16
+ });
17
+ res.status(201).json({
18
+ comment,
19
+ });
20
+ };
21
+ exports.POST = POST;
22
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL2NvbnRhY3QtcmVxdWVzdHMvW2lkXS9jb21tZW50cy9yb3V0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFDQSx3REFBaUU7QUFFakUsaURBQXdEO0FBRWpELE1BQU0sSUFBSSxHQUFHLEtBQUssRUFBRSxHQUFrQixFQUFFLEdBQW1CLEVBQUUsRUFBRTtJQUNwRSxNQUFNLElBQUksR0FBRyxrQ0FBcUIsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLElBQUksSUFBSSxFQUFFLENBQUMsQ0FBQTtJQUN4RCxNQUFNLE9BQU8sR0FBRyxHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FDL0Isa0NBQXNCLENBQ3ZCLENBQUE7SUFDRCxNQUFNLE9BQU8sR0FDVixHQUFXLENBQUMsWUFBWSxFQUFFLFFBQVE7UUFDbEMsR0FBVyxDQUFDLFlBQVksRUFBRSxPQUFPO1FBQ2xDLE9BQU8sQ0FBQTtJQUVULE1BQU0sT0FBTyxHQUFHLE1BQU0sT0FBTyxDQUFDLFVBQVUsQ0FBQztRQUN2QyxVQUFVLEVBQUUsR0FBRyxDQUFDLE1BQU0sQ0FBQyxFQUFFO1FBQ3pCLE9BQU8sRUFBRSxJQUFJLENBQUMsT0FBTztRQUNyQixRQUFRLEVBQUUsT0FBTztLQUNsQixDQUFDLENBQUE7SUFFRixHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQztRQUNuQixPQUFPO0tBQ1IsQ0FBQyxDQUFBO0FBQ0osQ0FBQyxDQUFBO0FBbkJZLFFBQUEsSUFBSSxRQW1CaEIifQ==
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PATCH = exports.GET = void 0;
7
+ const constants_1 = require("../../../../constants");
8
+ const update_contact_request_status_1 = __importDefault(require("../../../../workflows/update-contact-request-status"));
9
+ const validators_1 = require("../validators");
10
+ const GET = async (req, res) => {
11
+ const service = req.scope.resolve(constants_1.CONTACT_REQUEST_MODULE);
12
+ const { request, comments } = await service.retrieveWithComments(req.params.id);
13
+ res.status(200).json({
14
+ contact_request: request,
15
+ comments,
16
+ statuses: service.getStatusOptions(),
17
+ transitions: service.getNextStatuses(request.status),
18
+ });
19
+ };
20
+ exports.GET = GET;
21
+ const PATCH = async (req, res) => {
22
+ const body = validators_1.AdminUpdateContactStatusSchema.parse(req.body ?? {});
23
+ const actorId = req.auth_context?.actor_id ||
24
+ req.auth_context?.user_id ||
25
+ "admin";
26
+ const { result } = await (0, update_contact_request_status_1.default)(req.scope).run({
27
+ input: {
28
+ request_id: req.params.id,
29
+ status: body.status,
30
+ note: body.note,
31
+ actor_id: actorId,
32
+ },
33
+ });
34
+ const service = req.scope.resolve(constants_1.CONTACT_REQUEST_MODULE);
35
+ res.status(200).json({
36
+ contact_request: result.request,
37
+ transitions: service.getNextStatuses(result.request.status),
38
+ });
39
+ };
40
+ exports.PATCH = PATCH;
41
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL2NvbnRhY3QtcmVxdWVzdHMvW2lkXS9yb3V0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFDQSxxREFBOEQ7QUFFOUQsd0hBQW9HO0FBQ3BHLDhDQUE4RDtBQUV2RCxNQUFNLEdBQUcsR0FBRyxLQUFLLEVBQUUsR0FBa0IsRUFBRSxHQUFtQixFQUFFLEVBQUU7SUFDbkUsTUFBTSxPQUFPLEdBQUcsR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQy9CLGtDQUFzQixDQUN2QixDQUFBO0lBQ0QsTUFBTSxFQUFFLE9BQU8sRUFBRSxRQUFRLEVBQUUsR0FBRyxNQUFNLE9BQU8sQ0FBQyxvQkFBb0IsQ0FDOUQsR0FBRyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQ2QsQ0FBQTtJQUVELEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxDQUFDO1FBQ25CLGVBQWUsRUFBRSxPQUFPO1FBQ3hCLFFBQVE7UUFDUixRQUFRLEVBQUUsT0FBTyxDQUFDLGdCQUFnQixFQUFFO1FBQ3BDLFdBQVcsRUFBRSxPQUFPLENBQUMsZUFBZSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUM7S0FDckQsQ0FBQyxDQUFBO0FBQ0osQ0FBQyxDQUFBO0FBZFksUUFBQSxHQUFHLE9BY2Y7QUFFTSxNQUFNLEtBQUssR0FBRyxLQUFLLEVBQUUsR0FBa0IsRUFBRSxHQUFtQixFQUFFLEVBQUU7SUFDckUsTUFBTSxJQUFJLEdBQUcsMkNBQThCLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxJQUFJLElBQUksRUFBRSxDQUFDLENBQUE7SUFDakUsTUFBTSxPQUFPLEdBQ1YsR0FBVyxDQUFDLFlBQVksRUFBRSxRQUFRO1FBQ2xDLEdBQVcsQ0FBQyxZQUFZLEVBQUUsT0FBTztRQUNsQyxPQUFPLENBQUE7SUFFVCxNQUFNLEVBQUUsTUFBTSxFQUFFLEdBQUcsTUFBTSxJQUFBLHVDQUFrQyxFQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQyxHQUFHLENBQUM7UUFDekUsS0FBSyxFQUFFO1lBQ0wsVUFBVSxFQUFFLEdBQUcsQ0FBQyxNQUFNLENBQUMsRUFBRTtZQUN6QixNQUFNLEVBQUUsSUFBSSxDQUFDLE1BQU07WUFDbkIsSUFBSSxFQUFFLElBQUksQ0FBQyxJQUFJO1lBQ2YsUUFBUSxFQUFFLE9BQU87U0FDbEI7S0FDRixDQUFDLENBQUE7SUFFRixNQUFNLE9BQU8sR0FBRyxHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FDL0Isa0NBQXNCLENBQ3ZCLENBQUE7SUFFRCxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQztRQUNuQixlQUFlLEVBQUUsTUFBTSxDQUFDLE9BQU87UUFDL0IsV0FBVyxFQUFFLE9BQU8sQ0FBQyxlQUFlLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUM7S0FDNUQsQ0FBQyxDQUFBO0FBQ0osQ0FBQyxDQUFBO0FBeEJZLFFBQUEsS0FBSyxTQXdCakIifQ==
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GET = void 0;
4
+ const constants_1 = require("../../../constants");
5
+ const validators_1 = require("./validators");
6
+ const GET = async (req, res) => {
7
+ const query = validators_1.AdminListContactRequestsSchema.parse(req.query ?? {});
8
+ const service = req.scope.resolve(constants_1.CONTACT_REQUEST_MODULE);
9
+ const selector = {};
10
+ if (query.status) {
11
+ selector.status = query.status;
12
+ }
13
+ if (query.email || query.q) {
14
+ const email = (query.email ?? query.q).toLowerCase();
15
+ selector.email = email;
16
+ }
17
+ const [contactRequests, count] = await service.listWithFilters(selector, {
18
+ take: query.limit,
19
+ skip: query.offset,
20
+ order: { created_at: "DESC" },
21
+ });
22
+ res.status(200).json({
23
+ contact_requests: contactRequests,
24
+ count,
25
+ offset: query.offset,
26
+ limit: query.limit,
27
+ statuses: service.getStatusOptions(),
28
+ });
29
+ };
30
+ exports.GET = GET;
31
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL2NvbnRhY3QtcmVxdWVzdHMvcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQ0Esa0RBQTJEO0FBRTNELDZDQUE2RDtBQUV0RCxNQUFNLEdBQUcsR0FBRyxLQUFLLEVBQUUsR0FBa0IsRUFBRSxHQUFtQixFQUFFLEVBQUU7SUFDbkUsTUFBTSxLQUFLLEdBQUcsMkNBQThCLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxLQUFLLElBQUksRUFBRSxDQUFDLENBQUE7SUFDbkUsTUFBTSxPQUFPLEdBQUcsR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQy9CLGtDQUFzQixDQUN2QixDQUFBO0lBRUQsTUFBTSxRQUFRLEdBQTRCLEVBQUUsQ0FBQTtJQUU1QyxJQUFJLEtBQUssQ0FBQyxNQUFNLEVBQUUsQ0FBQztRQUNqQixRQUFRLENBQUMsTUFBTSxHQUFHLEtBQUssQ0FBQyxNQUFNLENBQUE7SUFDaEMsQ0FBQztJQUNELElBQUksS0FBSyxDQUFDLEtBQUssSUFBSSxLQUFLLENBQUMsQ0FBQyxFQUFFLENBQUM7UUFDM0IsTUFBTSxLQUFLLEdBQUcsQ0FBQyxLQUFLLENBQUMsS0FBSyxJQUFJLEtBQUssQ0FBQyxDQUFDLENBQUUsQ0FBQyxXQUFXLEVBQUUsQ0FBQTtRQUNyRCxRQUFRLENBQUMsS0FBSyxHQUFHLEtBQUssQ0FBQTtJQUN4QixDQUFDO0lBRUQsTUFBTSxDQUFDLGVBQWUsRUFBRSxLQUFLLENBQUMsR0FBRyxNQUFNLE9BQU8sQ0FBQyxlQUFlLENBQUMsUUFBUSxFQUFFO1FBQ3ZFLElBQUksRUFBRSxLQUFLLENBQUMsS0FBSztRQUNqQixJQUFJLEVBQUUsS0FBSyxDQUFDLE1BQU07UUFDbEIsS0FBSyxFQUFFLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRTtLQUM5QixDQUFDLENBQUE7SUFFRixHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQztRQUNuQixnQkFBZ0IsRUFBRSxlQUFlO1FBQ2pDLEtBQUs7UUFDTCxNQUFNLEVBQUUsS0FBSyxDQUFDLE1BQU07UUFDcEIsS0FBSyxFQUFFLEtBQUssQ0FBQyxLQUFLO1FBQ2xCLFFBQVEsRUFBRSxPQUFPLENBQUMsZ0JBQWdCLEVBQUU7S0FDckMsQ0FBQyxDQUFBO0FBQ0osQ0FBQyxDQUFBO0FBN0JZLFFBQUEsR0FBRyxPQTZCZiJ9
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AdminAddCommentSchema = exports.AdminUpdateContactStatusSchema = exports.AdminListContactRequestsSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ exports.AdminListContactRequestsSchema = zod_1.z.object({
6
+ status: zod_1.z.string().optional(),
7
+ email: zod_1.z.string().optional(),
8
+ q: zod_1.z.string().optional(),
9
+ limit: zod_1.z.coerce.number().min(1).max(100).default(20),
10
+ offset: zod_1.z.coerce.number().min(0).default(0),
11
+ });
12
+ exports.AdminUpdateContactStatusSchema = zod_1.z.object({
13
+ status: zod_1.z.string().min(1),
14
+ note: zod_1.z.string().optional(),
15
+ });
16
+ exports.AdminAddCommentSchema = zod_1.z.object({
17
+ comment: zod_1.z.string().min(2, "Comment must be at least 2 characters"),
18
+ });
19
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdG9ycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL3NyYy9hcGkvYWRtaW4vY29udGFjdC1yZXF1ZXN0cy92YWxpZGF0b3JzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLDZCQUF1QjtBQUVWLFFBQUEsOEJBQThCLEdBQUcsT0FBQyxDQUFDLE1BQU0sQ0FBQztJQUNyRCxNQUFNLEVBQUUsT0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLFFBQVEsRUFBRTtJQUM3QixLQUFLLEVBQUUsT0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLFFBQVEsRUFBRTtJQUM1QixDQUFDLEVBQUUsT0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLFFBQVEsRUFBRTtJQUN4QixLQUFLLEVBQUUsT0FBQyxDQUFDLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7SUFDcEQsTUFBTSxFQUFFLE9BQUMsQ0FBQyxNQUFNLENBQUMsTUFBTSxFQUFFLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUM7Q0FDNUMsQ0FBQyxDQUFBO0FBTVcsUUFBQSw4QkFBOEIsR0FBRyxPQUFDLENBQUMsTUFBTSxDQUFDO0lBQ3JELE1BQU0sRUFBRSxPQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUN6QixJQUFJLEVBQUUsT0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLFFBQVEsRUFBRTtDQUM1QixDQUFDLENBQUE7QUFNVyxRQUFBLHFCQUFxQixHQUFHLE9BQUMsQ0FBQyxNQUFNLENBQUM7SUFDNUMsT0FBTyxFQUFFLE9BQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxFQUFFLHVDQUF1QyxDQUFDO0NBQ3BFLENBQUMsQ0FBQSJ9
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GET = GET;
4
+ const constants_1 = require("../../../constants");
5
+ async function GET(req, res) {
6
+ const service = req.scope.resolve(constants_1.CONTACT_REQUEST_MODULE);
7
+ res.status(200).json({
8
+ contact_us: service.getOptions(),
9
+ });
10
+ }
11
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3BsdWdpbi9yb3V0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUlBLGtCQVdDO0FBZEQsa0RBQTJEO0FBR3BELEtBQUssVUFBVSxHQUFHLENBQ3ZCLEdBQWtCLEVBQ2xCLEdBQW1CO0lBRW5CLE1BQU0sT0FBTyxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUMvQixrQ0FBc0IsQ0FDdkIsQ0FBQTtJQUVELEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxDQUFDO1FBQ25CLFVBQVUsRUFBRSxPQUFPLENBQUMsVUFBVSxFQUFFO0tBQ2pDLENBQUMsQ0FBQTtBQUNKLENBQUMifQ==