medusa-contact-us 0.0.17 → 0.0.21

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