@mohasinac/appkit 2.6.6 → 2.6.8

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 (49) hide show
  1. package/dist/_internal/server/jobs/core/onSupportTicketCreate.d.ts +10 -0
  2. package/dist/_internal/server/jobs/core/onSupportTicketCreate.js +26 -0
  3. package/dist/_internal/server/jobs/core/onSupportTicketUpdate.d.ts +15 -0
  4. package/dist/_internal/server/jobs/core/onSupportTicketUpdate.js +50 -0
  5. package/dist/_internal/server/jobs/core/onUserBanChange.d.ts +17 -0
  6. package/dist/_internal/server/jobs/core/onUserBanChange.js +59 -0
  7. package/dist/_internal/server/jobs/handlers/index.d.ts +3 -0
  8. package/dist/_internal/server/jobs/handlers/index.js +4 -0
  9. package/dist/_internal/server/jobs/handlers/onSupportTicketCreate.d.ts +2 -0
  10. package/dist/_internal/server/jobs/handlers/onSupportTicketCreate.js +7 -0
  11. package/dist/_internal/server/jobs/handlers/onSupportTicketUpdate.d.ts +2 -0
  12. package/dist/_internal/server/jobs/handlers/onSupportTicketUpdate.js +8 -0
  13. package/dist/_internal/server/jobs/handlers/onUserBanChange.d.ts +2 -0
  14. package/dist/_internal/server/jobs/handlers/onUserBanChange.js +10 -0
  15. package/dist/client.d.ts +2 -2
  16. package/dist/client.js +1 -1
  17. package/dist/configs/next.js +12 -0
  18. package/dist/features/account/components/UserSupportView.d.ts +3 -0
  19. package/dist/features/account/components/UserSupportView.js +90 -0
  20. package/dist/features/account/components/index.d.ts +2 -0
  21. package/dist/features/account/components/index.js +1 -0
  22. package/dist/features/admin/components/AdminScammerEditorView.d.ts +15 -0
  23. package/dist/features/admin/components/AdminScammerEditorView.js +61 -0
  24. package/dist/features/admin/components/AdminScammersView.d.ts +4 -0
  25. package/dist/features/admin/components/AdminScammersView.js +147 -0
  26. package/dist/features/admin/components/index.d.ts +4 -0
  27. package/dist/features/admin/components/index.js +2 -0
  28. package/dist/features/admin/constants/filter-tabs.d.ts +17 -0
  29. package/dist/features/admin/constants/filter-tabs.js +8 -0
  30. package/dist/features/faq/schemas/firestore.js +1 -0
  31. package/dist/features/faq/types/index.d.ts +1 -1
  32. package/dist/features/layout/AppLayoutShell.d.ts +2 -1
  33. package/dist/features/layout/AppLayoutShell.js +10 -4
  34. package/dist/features/layout/TitleBarLayout.d.ts +3 -1
  35. package/dist/features/layout/TitleBarLayout.js +8 -4
  36. package/dist/index.d.ts +4 -0
  37. package/dist/index.js +4 -0
  38. package/dist/jobs.d.ts +1 -1
  39. package/dist/jobs.js +2 -0
  40. package/dist/next/routing/route-map.d.ts +14 -0
  41. package/dist/next/routing/route-map.js +7 -0
  42. package/dist/seed/faq-seed-data.js +303 -0
  43. package/dist/seed/users-seed-data.js +81 -3
  44. package/dist/tailwind-utilities.css +1 -1
  45. package/dist/ui/components/Badge.d.ts +1 -0
  46. package/dist/ui/components/Badge.js +1 -0
  47. package/dist/ui/components/Badge.style.css +11 -0
  48. package/dist/ui/components/RoleBadge.js +2 -0
  49. package/package.json +1 -1
@@ -0,0 +1,10 @@
1
+ import type { JobContext } from "../runtime/types";
2
+ export interface HandleSupportTicketCreateInput {
3
+ ticketId: string;
4
+ ticket: {
5
+ userId?: string;
6
+ subject?: string;
7
+ category?: string;
8
+ };
9
+ }
10
+ export declare function handleSupportTicketCreate(input: HandleSupportTicketCreateInput, ctx: JobContext): Promise<void>;
@@ -0,0 +1,26 @@
1
+ import { notificationRepository } from "../../../../repositories";
2
+ export async function handleSupportTicketCreate(input, ctx) {
3
+ const { ticketId, ticket } = input;
4
+ const userId = ticket.userId;
5
+ if (!userId) {
6
+ ctx.logger.warn("onSupportTicketCreate: no userId on ticket", { ticketId });
7
+ return;
8
+ }
9
+ // Confirm to the user their ticket was received
10
+ try {
11
+ await notificationRepository.create({
12
+ userId,
13
+ type: "account_action",
14
+ title: "Support ticket received",
15
+ body: `We received your support request: "${ticket.subject ?? "your ticket"}". We'll get back to you soon.`,
16
+ isRead: false,
17
+ entityId: ticketId,
18
+ entityType: "support_ticket",
19
+ createdAt: new Date(),
20
+ });
21
+ }
22
+ catch (err) {
23
+ ctx.logger.error("Failed to send ticket confirmation notification (non-fatal)", err, { ticketId, userId });
24
+ }
25
+ ctx.logger.info("onSupportTicketCreate complete", { ticketId, userId });
26
+ }
@@ -0,0 +1,15 @@
1
+ import type { JobContext } from "../runtime/types";
2
+ export interface HandleSupportTicketUpdateInput {
3
+ ticketId: string;
4
+ before: {
5
+ status?: string;
6
+ userId?: string;
7
+ subject?: string;
8
+ } | null;
9
+ after: {
10
+ status?: string;
11
+ userId?: string;
12
+ subject?: string;
13
+ } | null;
14
+ }
15
+ export declare function handleSupportTicketUpdate(input: HandleSupportTicketUpdateInput, ctx: JobContext): Promise<void>;
@@ -0,0 +1,50 @@
1
+ import { notificationRepository } from "../../../../repositories";
2
+ const USER_NOTIFY_STATUSES = new Set(["resolved", "closed", "waiting_on_user", "in_progress"]);
3
+ const STATUS_MESSAGES = {
4
+ resolved: {
5
+ title: "Support ticket resolved",
6
+ body: (subject) => `Your support ticket "${subject}" has been marked as resolved.`,
7
+ },
8
+ closed: {
9
+ title: "Support ticket closed",
10
+ body: (subject) => `Your support ticket "${subject}" has been closed.`,
11
+ },
12
+ waiting_on_user: {
13
+ title: "Response required on your ticket",
14
+ body: (subject) => `Your support ticket "${subject}" is waiting for your reply.`,
15
+ },
16
+ in_progress: {
17
+ title: "Support ticket in progress",
18
+ body: (subject) => `Your support ticket "${subject}" is being reviewed by our team.`,
19
+ },
20
+ };
21
+ export async function handleSupportTicketUpdate(input, ctx) {
22
+ const { ticketId, before, after } = input;
23
+ const prevStatus = before?.status;
24
+ const nextStatus = after?.status;
25
+ const userId = after?.userId ?? before?.userId;
26
+ const subject = after?.subject ?? before?.subject ?? "your ticket";
27
+ if (!nextStatus || !userId || prevStatus === nextStatus)
28
+ return;
29
+ if (!USER_NOTIFY_STATUSES.has(nextStatus))
30
+ return;
31
+ const msg = STATUS_MESSAGES[nextStatus];
32
+ if (!msg)
33
+ return;
34
+ try {
35
+ await notificationRepository.create({
36
+ userId,
37
+ type: "account_action",
38
+ title: msg.title,
39
+ body: msg.body(subject),
40
+ isRead: false,
41
+ entityId: ticketId,
42
+ entityType: "support_ticket",
43
+ createdAt: new Date(),
44
+ });
45
+ }
46
+ catch (err) {
47
+ ctx.logger.error("Failed to notify user of ticket status change (non-fatal)", err, { ticketId, userId, nextStatus });
48
+ }
49
+ ctx.logger.info("onSupportTicketUpdate complete", { ticketId, userId, prevStatus, nextStatus });
50
+ }
@@ -0,0 +1,17 @@
1
+ import type { JobContext } from "../runtime/types";
2
+ export interface HandleUserBanChangeInput {
3
+ uid: string;
4
+ before: {
5
+ isDisabled?: boolean;
6
+ hardBanReason?: string;
7
+ hardBannedBy?: string;
8
+ softBans?: unknown[];
9
+ } | null;
10
+ after: {
11
+ isDisabled?: boolean;
12
+ hardBanReason?: string;
13
+ hardBannedBy?: string;
14
+ softBans?: unknown[];
15
+ } | null;
16
+ }
17
+ export declare function handleUserBanChange(input: HandleUserBanChangeInput, ctx: JobContext): Promise<void>;
@@ -0,0 +1,59 @@
1
+ export async function handleUserBanChange(input, ctx) {
2
+ const { uid, before, after } = input;
3
+ if (!after)
4
+ return;
5
+ const entries = [];
6
+ const wasDisabled = Boolean(before?.isDisabled);
7
+ const isNowDisabled = Boolean(after.isDisabled);
8
+ // Hard ban applied
9
+ if (!wasDisabled && isNowDisabled && after.hardBanReason) {
10
+ entries.push({
11
+ type: "hard_ban",
12
+ reason: after.hardBanReason,
13
+ performedBy: after.hardBannedBy,
14
+ at: new Date(),
15
+ });
16
+ }
17
+ // Hard ban lifted
18
+ if (wasDisabled && !isNowDisabled) {
19
+ entries.push({
20
+ type: "hard_unban",
21
+ at: new Date(),
22
+ });
23
+ }
24
+ // Soft ban changes — detect additions
25
+ const prevSoftBans = before?.softBans ?? [];
26
+ const nextSoftBans = after.softBans ?? [];
27
+ const prevActions = new Set(prevSoftBans.map((b) => b.action));
28
+ const nextActions = new Set(nextSoftBans.map((b) => b.action));
29
+ for (const nextBan of nextSoftBans) {
30
+ if (!prevActions.has(nextBan.action)) {
31
+ entries.push({
32
+ type: "soft_ban",
33
+ action: nextBan.action,
34
+ reason: nextBan.reason,
35
+ performedBy: nextBan.bannedBy,
36
+ at: new Date(),
37
+ });
38
+ }
39
+ }
40
+ for (const action of prevActions) {
41
+ if (!nextActions.has(action)) {
42
+ entries.push({ type: "soft_unban", action, at: new Date() });
43
+ }
44
+ }
45
+ if (entries.length === 0)
46
+ return;
47
+ const banHistoryRef = ctx.db.collection("users").doc(uid).collection("banHistory");
48
+ const batch = ctx.db.batch();
49
+ for (const entry of entries) {
50
+ batch.set(banHistoryRef.doc(), entry);
51
+ }
52
+ try {
53
+ await batch.commit();
54
+ ctx.logger.info("onUserBanChange: ban history entries written", { uid, count: entries.length });
55
+ }
56
+ catch (err) {
57
+ ctx.logger.error("onUserBanChange: failed to write ban history (non-fatal)", err, { uid });
58
+ }
59
+ }
@@ -41,3 +41,6 @@ export { bundleStockSyncHandler } from "./bundleStockSync";
41
41
  export { onProductStockChangeHandler } from "./onProductStockChange";
42
42
  export { triggerEventRaffleHandler } from "./triggerEventRaffle";
43
43
  export { assignSpinPrizeHandler } from "./assignSpinPrize";
44
+ export { onSupportTicketCreateHandler } from "./onSupportTicketCreate";
45
+ export { onSupportTicketUpdateHandler } from "./onSupportTicketUpdate";
46
+ export { onUserBanChangeHandler } from "./onUserBanChange";
@@ -46,3 +46,7 @@ export { bundleStockSyncHandler } from "./bundleStockSync";
46
46
  export { onProductStockChangeHandler } from "./onProductStockChange";
47
47
  export { triggerEventRaffleHandler } from "./triggerEventRaffle";
48
48
  export { assignSpinPrizeHandler } from "./assignSpinPrize";
49
+ // BAN9 — support ticket lifecycle + user ban audit
50
+ export { onSupportTicketCreateHandler } from "./onSupportTicketCreate";
51
+ export { onSupportTicketUpdateHandler } from "./onSupportTicketUpdate";
52
+ export { onUserBanChangeHandler } from "./onUserBanChange";
@@ -0,0 +1,2 @@
1
+ import type { FirestoreTriggerHandler } from "../runtime/types";
2
+ export declare const onSupportTicketCreateHandler: FirestoreTriggerHandler<null, Record<string, unknown>>;
@@ -0,0 +1,7 @@
1
+ import { handleSupportTicketCreate } from "../core/onSupportTicketCreate";
2
+ export const onSupportTicketCreateHandler = async (event, ctx) => {
3
+ const ticket = event.after;
4
+ if (!ticket)
5
+ return;
6
+ await handleSupportTicketCreate({ ticketId: event.params.ticketId, ticket }, ctx);
7
+ };
@@ -0,0 +1,2 @@
1
+ import type { FirestoreTriggerHandler } from "../runtime/types";
2
+ export declare const onSupportTicketUpdateHandler: FirestoreTriggerHandler<Record<string, unknown>, Record<string, unknown>>;
@@ -0,0 +1,8 @@
1
+ import { handleSupportTicketUpdate } from "../core/onSupportTicketUpdate";
2
+ export const onSupportTicketUpdateHandler = async (event, ctx) => {
3
+ await handleSupportTicketUpdate({
4
+ ticketId: event.params.ticketId,
5
+ before: event.before,
6
+ after: event.after,
7
+ }, ctx);
8
+ };
@@ -0,0 +1,2 @@
1
+ import type { FirestoreTriggerHandler } from "../runtime/types";
2
+ export declare const onUserBanChangeHandler: FirestoreTriggerHandler<Record<string, unknown>, Record<string, unknown>>;
@@ -0,0 +1,10 @@
1
+ import { handleUserBanChange } from "../core/onUserBanChange";
2
+ export const onUserBanChangeHandler = async (event, ctx) => {
3
+ const before = event.before;
4
+ const after = event.after;
5
+ const didBanChange = before?.isDisabled !== after?.isDisabled ||
6
+ (before?.softBans?.length ?? 0) !== (after?.softBans?.length ?? 0);
7
+ if (!didBanChange)
8
+ return;
9
+ await handleUserBanChange({ uid: event.params.uid, before, after }, ctx);
10
+ };
package/dist/client.d.ts CHANGED
@@ -151,8 +151,8 @@ export type { SellerDashboardViewProps as StoreDashboardViewProps, SellerDashboa
151
151
  export { SellerPayoutSettingsView, SellerShippingView, SellerReviewsView, SellerPayoutRequestView, SellerAnalyticsStats, SellerTopProducts, SellerAnalyticsView, SellerPayoutsView, SellerCouponEditorView, SellerBidsView, SellerAddressesView, SellerPreOrdersView, SellerPrizeDrawsView } from "./features/seller/components/index";
152
152
  export type { SellerPayoutSettingsViewProps, SellerShippingViewProps, SellerReviewsViewProps, SellerPayoutRequestViewProps, SellerAnalyticsViewProps, SellerPayoutsViewProps, SellerCouponEditorViewProps, CouponEditorDraft, SellerBidsViewProps, SellerAddressesViewProps, SellerPreOrdersViewProps, SellerPrizeDrawsViewProps } from "./features/seller/components/index";
153
153
  export type { SellerAnalyticsSummary, SellerAnalyticsTopProduct } from "./features/seller/types/index";
154
- export { UserAccountHubView, UserOrdersView, OrderDetailView, UserNotificationsView, UserReturnsView } from "./features/account/index";
155
- export type { UserAccountHubViewProps, UserAccountHubViewLabels, UserOrdersViewProps, UserOrdersViewLabels, OrderDetailViewProps, OrderDetailViewLabels, UserNotificationsViewProps, UserNotificationsViewLabels, UserReturnsViewProps, UserReturnsViewLabels } from "./features/account/index";
154
+ export { UserAccountHubView, UserOrdersView, OrderDetailView, UserNotificationsView, UserReturnsView, UserSupportView } from "./features/account/index";
155
+ export type { UserAccountHubViewProps, UserAccountHubViewLabels, UserOrdersViewProps, UserOrdersViewLabels, OrderDetailViewProps, OrderDetailViewLabels, UserNotificationsViewProps, UserNotificationsViewLabels, UserReturnsViewProps, UserReturnsViewLabels, UserSupportViewProps } from "./features/account/index";
156
156
  export { useOrders, useOrder, OrdersList } from "./features/orders/index";
157
157
  export { useCouponValidate } from "./features/promotions/hooks/useCouponValidate";
158
158
  export { BlogPostView } from "./features/blog/components/BlogPostView";
package/dist/client.js CHANGED
@@ -181,7 +181,7 @@ export { SellerOffersPanel } from "./features/seller/components/SellerOffersPane
181
181
  export { UserOffersPanel } from "./features/account/components/UserOffersPanel";
182
182
  export { SellerDashboardView as StoreDashboardView, SellerDashboardView, useSellerDashboard as useStoreDashboard, useSellerDashboard } from "./features/seller/index";
183
183
  export { SellerPayoutSettingsView, SellerShippingView, SellerReviewsView, SellerPayoutRequestView, SellerAnalyticsStats, SellerTopProducts, SellerAnalyticsView, SellerPayoutsView, SellerCouponEditorView, SellerBidsView, SellerAddressesView, SellerPreOrdersView, SellerPrizeDrawsView } from "./features/seller/components/index";
184
- export { UserAccountHubView, UserOrdersView, OrderDetailView, UserNotificationsView, UserReturnsView } from "./features/account/index";
184
+ export { UserAccountHubView, UserOrdersView, OrderDetailView, UserNotificationsView, UserReturnsView, UserSupportView } from "./features/account/index";
185
185
  export { useOrders, useOrder, OrdersList } from "./features/orders/index";
186
186
  export { useCouponValidate } from "./features/promotions/hooks/useCouponValidate";
187
187
  export { BlogPostView } from "./features/blog/components/BlogPostView";
@@ -93,6 +93,18 @@ export function defineNextConfig(override = {}) {
93
93
  "./node_modules/gtoken/**",
94
94
  "./node_modules/jws/**",
95
95
  "./node_modules/gaxios/**",
96
+ // gRPC transport layer used by @google-cloud/firestore via google-gax
97
+ "./node_modules/@grpc/**",
98
+ // Transitive deps of google-gax / @grpc hoisted to root node_modules
99
+ "./node_modules/protobufjs/**",
100
+ "./node_modules/object-hash/**",
101
+ "./node_modules/proto3-json-serializer/**",
102
+ "./node_modules/long/**",
103
+ "./node_modules/node-fetch/**",
104
+ "./node_modules/abort-controller/**",
105
+ "./node_modules/retry-request/**",
106
+ "./node_modules/duplexify/**",
107
+ "./node_modules/uuid/**",
96
108
  ],
97
109
  };
98
110
  const mergedOutputFileTracingIncludes = {
@@ -0,0 +1,3 @@
1
+ export interface UserSupportViewProps {
2
+ }
3
+ export declare function UserSupportView(_props: UserSupportViewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,90 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
5
+ import { Button, Div, FormActions, Select, SideDrawer, Text, useToast, } from "../../../ui";
6
+ import { apiClient } from "../../../http";
7
+ import { SUPPORT_ENDPOINTS } from "../../../constants/api-endpoints";
8
+ // --- Constants ---------------------------------------------------------------
9
+ const CATEGORY_OPTIONS = [
10
+ { label: "Order Issue", value: "order_issue" },
11
+ { label: "Billing / Payment", value: "billing_payment" },
12
+ { label: "Account", value: "account" },
13
+ { label: "Listing Dispute", value: "listing_dispute" },
14
+ { label: "Refund Request", value: "refund_request" },
15
+ { label: "Auction Dispute", value: "auction_dispute" },
16
+ { label: "General", value: "general" },
17
+ ];
18
+ const STATUS_BADGE = {
19
+ open: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300",
20
+ in_progress: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300",
21
+ waiting_on_user: "bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300",
22
+ resolved: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300",
23
+ closed: "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400",
24
+ };
25
+ const ROLE_LABEL = {
26
+ user: "You",
27
+ support: "Support",
28
+ admin: "Admin",
29
+ };
30
+ export function UserSupportView(_props) {
31
+ const queryClient = useQueryClient();
32
+ const { showToast } = useToast();
33
+ const [detailOpen, setDetailOpen] = useState(false);
34
+ const [selectedTicket, setSelectedTicket] = useState(null);
35
+ const [newTicketOpen, setNewTicketOpen] = useState(false);
36
+ // New ticket form state
37
+ const [newSubject, setNewSubject] = useState("");
38
+ const [newCategory, setNewCategory] = useState("general");
39
+ const [newDescription, setNewDescription] = useState("");
40
+ const [newOrderId, setNewOrderId] = useState("");
41
+ // Reply state
42
+ const [replyBody, setReplyBody] = useState("");
43
+ const { data, isLoading, error } = useQuery({
44
+ queryKey: ["user", "support-tickets"],
45
+ queryFn: () => apiClient.get(SUPPORT_ENDPOINTS.TICKETS),
46
+ });
47
+ const tickets = data?.tickets ?? [];
48
+ const invalidate = () => queryClient.invalidateQueries({ queryKey: ["user", "support-tickets"] });
49
+ const createMutation = useMutation({
50
+ mutationFn: async () => {
51
+ await apiClient.post(SUPPORT_ENDPOINTS.TICKETS, {
52
+ subject: newSubject.trim(),
53
+ category: newCategory,
54
+ description: newDescription.trim(),
55
+ orderId: newOrderId.trim() || undefined,
56
+ });
57
+ },
58
+ onSuccess: () => {
59
+ showToast("Support ticket created.", "success");
60
+ setNewTicketOpen(false);
61
+ setNewSubject("");
62
+ setNewCategory("general");
63
+ setNewDescription("");
64
+ setNewOrderId("");
65
+ invalidate();
66
+ },
67
+ onError: (err) => {
68
+ showToast(err?.message ?? "Failed to create ticket.", "error");
69
+ },
70
+ });
71
+ const replyMutation = useMutation({
72
+ mutationFn: async () => {
73
+ await apiClient.post(SUPPORT_ENDPOINTS.TICKET_MESSAGES(selectedTicket.id), { body: replyBody.trim() });
74
+ },
75
+ onSuccess: () => {
76
+ showToast("Reply sent.", "success");
77
+ setReplyBody("");
78
+ invalidate();
79
+ },
80
+ onError: (err) => {
81
+ showToast(err?.message ?? "Failed to send reply.", "error");
82
+ },
83
+ });
84
+ return (_jsxs(_Fragment, { children: [_jsxs(Div, { className: "mx-auto max-w-2xl px-4 py-6", children: [_jsxs(Div, { className: "mb-4 flex items-center justify-between", children: [_jsx(Text, { className: "text-xl font-semibold text-zinc-900 dark:text-zinc-100", children: "Support Tickets" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: () => setNewTicketOpen(true), children: "New ticket" })] }), isLoading && (_jsx(Div, { className: "space-y-3", children: [1, 2, 3].map((i) => (_jsx("div", { className: "h-16 animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800" }, i))) })), error && (_jsx(Div, { className: "rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: "Failed to load support tickets." })), !isLoading && tickets.length === 0 && (_jsx(Div, { className: "rounded-xl border border-zinc-200 bg-zinc-50 px-6 py-10 text-center dark:border-zinc-700 dark:bg-zinc-900/40", children: _jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "You haven't opened any support tickets yet." }) })), _jsx("ul", { className: "space-y-3", children: tickets.map((ticket) => (_jsx("li", { children: _jsx("button", { type: "button", className: "w-full rounded-xl border border-zinc-200 bg-white p-4 text-left shadow-sm hover:border-primary-300 transition-colors dark:border-zinc-700 dark:bg-zinc-900", onClick: () => {
85
+ setSelectedTicket(ticket);
86
+ setDetailOpen(true);
87
+ }, children: _jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "font-medium text-zinc-900 dark:text-zinc-100 truncate", children: ticket.subject }), _jsxs("p", { className: "text-xs text-zinc-500 dark:text-zinc-400", children: [ticket.category.replace(/_/g, " "), ticket.orderId ? ` · Order: ${ticket.orderId}` : ""] })] }), _jsx("span", { className: `shrink-0 inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium ${STATUS_BADGE[ticket.status] ?? STATUS_BADGE.open}`, children: ticket.status.replace(/_/g, " ") })] }) }) }, ticket.id))) })] }), _jsx(SideDrawer, { isOpen: newTicketOpen, onClose: () => setNewTicketOpen(false), title: "Open a support ticket", children: _jsxs(Div, { className: "flex flex-col gap-4 p-4", children: [_jsx(Select, { label: "Category", options: CATEGORY_OPTIONS, value: newCategory, onValueChange: setNewCategory }), _jsxs(Div, { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Subject" }), _jsx("input", { type: "text", value: newSubject, onChange: (e) => setNewSubject(e.target.value), placeholder: "Brief description of the issue", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500" })] }), newCategory === "order_issue" && (_jsxs(Div, { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Order ID" }), _jsx("input", { type: "text", value: newOrderId, onChange: (e) => setNewOrderId(e.target.value), placeholder: "e.g. order-3-20260508-a1b2c3", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500" })] })), _jsxs(Div, { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Description" }), _jsx("textarea", { value: newDescription, onChange: (e) => setNewDescription(e.target.value), rows: 4, placeholder: "Describe the issue in detail\u2026", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500" })] }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: () => setNewTicketOpen(false), children: "Cancel" }), _jsx(Button, { type: "button", isLoading: createMutation.isPending, disabled: !newSubject.trim() || !newDescription.trim() || createMutation.isPending, onClick: () => createMutation.mutate(), children: "Submit ticket" })] })] }) }), _jsx(SideDrawer, { isOpen: detailOpen, onClose: () => setDetailOpen(false), title: selectedTicket?.subject ?? "Ticket", children: selectedTicket && (_jsxs(Div, { className: "flex flex-col gap-4 p-4", children: [_jsxs(Div, { className: "flex flex-wrap gap-2", children: [_jsx("span", { className: `inline-flex rounded-full px-2.5 py-1 text-xs font-medium ${STATUS_BADGE[selectedTicket.status] ?? STATUS_BADGE.open}`, children: selectedTicket.status.replace(/_/g, " ") }), _jsx("span", { className: "rounded-full bg-zinc-100 px-2.5 py-1 text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300", children: selectedTicket.category.replace(/_/g, " ") }), selectedTicket.orderId && (_jsxs("span", { className: "rounded-full bg-zinc-100 px-2.5 py-1 text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300", children: ["Order: ", selectedTicket.orderId] }))] }), selectedTicket.description && (_jsxs(Div, { className: "rounded-lg border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-900/40", children: [_jsx(Text, { className: "mb-1 text-xs font-semibold text-zinc-500 uppercase tracking-wide", children: "Your description" }), _jsx(Text, { className: "whitespace-pre-wrap text-sm text-zinc-700 dark:text-zinc-200", children: selectedTicket.description })] })), (selectedTicket.messages ?? []).length > 0 && (_jsxs(Div, { className: "space-y-2", children: [_jsx(Text, { className: "text-xs font-semibold text-zinc-500 uppercase tracking-wide", children: "Messages" }), _jsx("div", { className: "space-y-2 max-h-72 overflow-y-auto", children: (selectedTicket.messages ?? []).map((msg, i) => (_jsxs("div", { className: `rounded-lg p-3 text-sm ${msg.authorRole === "user"
88
+ ? "bg-zinc-50 border border-zinc-200 dark:bg-zinc-900/40 dark:border-zinc-700"
89
+ : "bg-blue-50 border border-blue-200 dark:bg-blue-900/20 dark:border-blue-800"}`, children: [_jsxs("div", { className: "mb-1 flex items-center gap-2 text-xs text-zinc-400 dark:text-zinc-500", children: [_jsx("span", { className: "font-medium text-zinc-600 dark:text-zinc-300", children: ROLE_LABEL[msg.authorRole ?? "user"] ?? msg.authorRole }), msg.createdAt && (_jsx("span", { children: new Date(msg.createdAt).toLocaleString() }))] }), _jsx("p", { className: "whitespace-pre-wrap text-zinc-700 dark:text-zinc-200", children: msg.body })] }, msg.id ?? i))) })] })), selectedTicket.status !== "closed" && selectedTicket.status !== "resolved" && (_jsxs(Div, { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Reply" }), _jsx("textarea", { value: replyBody, onChange: (e) => setReplyBody(e.target.value), rows: 3, placeholder: "Add a message to your ticket\u2026", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", isLoading: replyMutation.isPending, disabled: !replyBody.trim() || replyMutation.isPending, onClick: () => replyMutation.mutate(), children: "Send reply" })] }))] })) })] }));
90
+ }
@@ -38,3 +38,5 @@ export { UserOrderTrackView } from "./UserOrderTrackView";
38
38
  export type { UserOrderTrackViewProps, UserOrderTrackViewLabels, } from "./UserOrderTrackView";
39
39
  export { UserReturnsView } from "./UserReturnsView";
40
40
  export type { UserReturnsViewProps, UserReturnsViewLabels, } from "./UserReturnsView";
41
+ export { UserSupportView } from "./UserSupportView";
42
+ export type { UserSupportViewProps } from "./UserSupportView";
@@ -18,3 +18,4 @@ export { ChatWindow } from "./ChatWindow";
18
18
  export { BecomeSellerView } from "./BecomeSellerView";
19
19
  export { UserOrderTrackView } from "./UserOrderTrackView";
20
20
  export { UserReturnsView } from "./UserReturnsView";
21
+ export { UserSupportView } from "./UserSupportView";
@@ -0,0 +1,15 @@
1
+ export interface AdminScammerEditorViewProps {
2
+ open: boolean;
3
+ onClose: () => void;
4
+ scammerId?: string;
5
+ displayNames?: string[];
6
+ scamType?: string;
7
+ description?: string;
8
+ phones?: string[];
9
+ upiIds?: string[];
10
+ currentStatus?: string;
11
+ verificationNote?: string;
12
+ reportedBy?: string;
13
+ reportedByAnon?: boolean;
14
+ }
15
+ export declare function AdminScammerEditorView({ open, onClose, scammerId, displayNames, scamType, description, phones, upiIds, currentStatus, verificationNote, reportedBy, reportedByAnon, }: AdminScammerEditorViewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
5
+ import { Button, Div, FormActions, Select, SideDrawer, Text, useToast, } from "../../../ui";
6
+ import { apiClient } from "../../../http";
7
+ import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
8
+ const STATUS_OPTIONS = [
9
+ { label: "Pending review", value: "pending_review" },
10
+ { label: "Verified", value: "verified" },
11
+ { label: "Rejected", value: "rejected" },
12
+ { label: "Removed", value: "removed" },
13
+ ];
14
+ const STATUS_COLOR = {
15
+ pending_review: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300",
16
+ verified: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300",
17
+ rejected: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300",
18
+ removed: "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400",
19
+ };
20
+ export function AdminScammerEditorView({ open, onClose, scammerId, displayNames = [], scamType, description, phones = [], upiIds = [], currentStatus, verificationNote, reportedBy, reportedByAnon, }) {
21
+ const queryClient = useQueryClient();
22
+ const { showToast } = useToast();
23
+ const [status, setStatus] = React.useState(currentStatus ?? "pending_review");
24
+ const [note, setNote] = React.useState(verificationNote ?? "");
25
+ React.useEffect(() => {
26
+ if (open) {
27
+ setStatus(currentStatus ?? "pending_review");
28
+ setNote(verificationNote ?? "");
29
+ }
30
+ }, [open, currentStatus, verificationNote]);
31
+ const updateMutation = useMutation({
32
+ mutationFn: async () => {
33
+ await apiClient.patch(ADMIN_ENDPOINTS.SCAMMER_BY_ID(scammerId), {
34
+ status,
35
+ verificationNote: note || undefined,
36
+ });
37
+ },
38
+ onSuccess: () => {
39
+ showToast("Scammer profile updated.", "success");
40
+ queryClient.invalidateQueries({ queryKey: ["admin", "scammers"] });
41
+ onClose();
42
+ },
43
+ onError: (err) => {
44
+ showToast(err?.message ?? "Failed to update profile.", "error");
45
+ },
46
+ });
47
+ const deleteMutation = useMutation({
48
+ mutationFn: async () => {
49
+ await apiClient.delete(ADMIN_ENDPOINTS.SCAMMER_BY_ID(scammerId));
50
+ },
51
+ onSuccess: () => {
52
+ showToast("Scammer profile deleted.", "success");
53
+ queryClient.invalidateQueries({ queryKey: ["admin", "scammers"] });
54
+ onClose();
55
+ },
56
+ onError: (err) => {
57
+ showToast(err?.message ?? "Failed to delete profile.", "error");
58
+ },
59
+ });
60
+ return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: displayNames.length > 0 ? displayNames[0] : "Scammer Profile", children: _jsxs(Div, { className: "flex flex-col gap-4 p-4", children: [_jsxs(Div, { className: "flex items-center gap-2", children: [_jsx("span", { className: `inline-flex rounded-full px-2.5 py-1 text-xs font-medium ${STATUS_COLOR[currentStatus ?? "pending_review"] ?? STATUS_COLOR.pending_review}`, children: (currentStatus ?? "pending_review").replace(/_/g, " ") }), scamType && (_jsx("span", { className: "rounded-full bg-zinc-100 px-2.5 py-1 text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300", children: scamType.replace(/_/g, " ") }))] }), displayNames.length > 0 && (_jsxs(Div, { children: [_jsx(Text, { className: "mb-1 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: "Names / Aliases" }), _jsx("div", { className: "flex flex-wrap gap-1", children: displayNames.map((name, i) => (_jsx("span", { className: "rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs dark:bg-zinc-800", children: name }, i))) })] })), (phones.length > 0 || upiIds.length > 0) && (_jsxs(Div, { className: "rounded-lg border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-900/40", children: [phones.length > 0 && (_jsxs("div", { className: "mb-2", children: [_jsx(Text, { className: "mb-1 text-xs font-semibold uppercase tracking-wide text-zinc-500", children: "Phone numbers" }), _jsx("div", { className: "flex flex-wrap gap-1", children: phones.map((p, i) => (_jsx("code", { className: "rounded bg-zinc-200 px-1.5 py-0.5 text-xs dark:bg-zinc-700", children: p }, i))) })] })), upiIds.length > 0 && (_jsxs("div", { children: [_jsx(Text, { className: "mb-1 text-xs font-semibold uppercase tracking-wide text-zinc-500", children: "UPI IDs" }), _jsx("div", { className: "flex flex-wrap gap-1", children: upiIds.map((u, i) => (_jsx("code", { className: "rounded bg-zinc-200 px-1.5 py-0.5 text-xs dark:bg-zinc-700", children: u }, i))) })] }))] })), description && (_jsxs(Div, { className: "rounded-lg border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-900/40", children: [_jsx(Text, { className: "mb-1 text-xs font-semibold uppercase tracking-wide text-zinc-500", children: "Description" }), _jsx(Text, { className: "whitespace-pre-wrap text-sm text-zinc-700 dark:text-zinc-200", children: description })] })), _jsxs(Div, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: ["Reported by:", " ", _jsx("span", { className: "font-medium text-zinc-700 dark:text-zinc-300", children: reportedByAnon ? "Anonymous" : (reportedBy ?? "Unknown") })] }), _jsx("hr", { className: "border-zinc-200 dark:border-zinc-700" }), _jsx(Select, { label: "Status", options: STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsxs(Div, { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: "Verification note (internal)" }), _jsx("textarea", { value: note, onChange: (e) => setNote(e.target.value), rows: 3, placeholder: "e.g. Verified via 3 independent reports\u2026", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500" })] }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "danger", size: "sm", disabled: !scammerId, isLoading: deleteMutation.isPending, onClick: () => deleteMutation.mutate(), children: "Delete" }), _jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "button", isLoading: updateMutation.isPending, disabled: !scammerId || updateMutation.isPending, onClick: () => updateMutation.mutate(), children: "Save changes" })] })] }) }));
61
+ }
@@ -0,0 +1,4 @@
1
+ import type { ListingViewShellProps } from "../../../ui";
2
+ export interface AdminScammersViewProps extends ListingViewShellProps {
3
+ }
4
+ export declare function AdminScammersView({ children, ...props }: AdminScammersViewProps): import("react/jsx-runtime").JSX.Element;