@mohasinac/appkit 4.31.0 → 4.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/products/list-public.d.ts +18 -0
- package/dist/_internal/server/features/products/list-public.js +27 -9
- package/dist/client.d.ts +9 -0
- package/dist/client.js +12 -0
- package/dist/features/admin/components/AdminAdsView.js +36 -34
- package/dist/features/admin/components/AdminCarouselEditorView.js +78 -10
- package/dist/features/admin/components/AdminOrderEditorView.js +55 -24
- package/dist/features/admin/components/AdminProductEditorView.js +78 -18
- package/dist/features/admin/components/AdminSectionsView.js +89 -32
- package/dist/features/admin/components/AdminSiteSettingsView.js +96 -44
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +58 -43
- package/dist/features/admin/components/AdminUserEditorView.js +65 -82
- package/dist/features/admin/constants/admin-page-tabs.d.ts +36 -0
- package/dist/features/admin/constants/admin-page-tabs.js +45 -0
- package/dist/features/admin/schemas/admin-ops-forms.d.ts +81 -0
- package/dist/features/admin/schemas/admin-ops-forms.js +123 -1
- package/dist/features/admin/schemas/admin-user-form.d.ts +78 -3
- package/dist/features/admin/schemas/admin-user-form.js +96 -7
- package/dist/features/admin/schemas/homepage-section-form.d.ts +9 -0
- package/dist/features/layout/PageTabs.d.ts +17 -0
- package/dist/features/layout/PageTabs.js +45 -0
- package/dist/features/products/components/GroupSettingsPanel.js +62 -16
- package/dist/features/search/action-index/tab-entries.d.ts +18 -0
- package/dist/features/search/action-index/tab-entries.js +128 -0
- package/dist/features/seller/components/SellerGoogleReviewsView.js +31 -19
- package/dist/features/seller/components/SellerOrdersView.js +57 -19
- package/dist/features/seller/components/SellerReviewsView.d.ts +2 -1
- package/dist/features/seller/components/SellerReviewsView.js +54 -19
- package/dist/features/seller/constants/store-tabs.d.ts +64 -0
- package/dist/features/seller/constants/store-tabs.js +60 -0
- package/dist/features/seller/schemas/order-forms.d.ts +33 -0
- package/dist/features/seller/schemas/order-forms.js +75 -0
- package/dist/features/seller/schemas/review-forms.d.ts +37 -0
- package/dist/features/seller/schemas/review-forms.js +82 -0
- package/dist/features/shell/index.d.ts +1 -1
- package/dist/features/shell/index.js +5 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +44 -15
- package/dist/features/shipments/schemas/validation.d.ts +41 -0
- package/dist/features/shipments/schemas/validation.js +49 -0
- package/dist/features/store-extensions/schemas/google-config-form.d.ts +36 -0
- package/dist/features/store-extensions/schemas/google-config-form.js +24 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +67 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +53 -22
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/next/index.d.ts +8 -0
- package/dist/next/index.js +37 -0
- package/dist/react/hooks/useTabParam.d.ts +19 -0
- package/dist/react/hooks/useTabParam.js +65 -0
- package/dist/schemas/registry.d.ts +27 -3
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/package.json +1 -1
|
@@ -3,9 +3,14 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { Code, useApiMutation } from "@mohasinac/appkit/client";
|
|
4
4
|
import React from "react";
|
|
5
5
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
6
|
-
import { Button, Div,
|
|
6
|
+
import { Button, Div, HorizontalRule, Input, Label, Row, Select, SideDrawer, Span, Stack, Text, Textarea, Toggle, useToast } from "../../../ui";
|
|
7
7
|
import { apiClient } from "../../../http";
|
|
8
8
|
import { ADMIN_ENDPOINTS, SUPPORT_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
9
|
+
import { supportTicketUpdateSchema, toRelatedPartiesPayload, } from "../schemas/admin-ops-forms";
|
|
10
|
+
import { SectionForm, useSectionFormNav, buildSectionsFromSchema, visibleValues, } from "../../shell";
|
|
11
|
+
import { useFormShellState, FormShellContext } from "../../../ui/forms";
|
|
12
|
+
import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
|
|
13
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
9
14
|
import { RecordStatusTimeline } from "../../status-history/components/RecordStatusTimeline";
|
|
10
15
|
const __P = {
|
|
11
16
|
p3: "p-[var(--appkit-space-3)]",
|
|
@@ -14,6 +19,23 @@ const __P = {
|
|
|
14
19
|
const __O = {
|
|
15
20
|
yAuto: "overflow-y-auto",
|
|
16
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* The drawer reopens for a different ticket without unmounting, so the draft is
|
|
24
|
+
* seeded in two places — initial state and the `open` effect. One function, so
|
|
25
|
+
* the two cannot disagree about what an absent field defaults to.
|
|
26
|
+
*/
|
|
27
|
+
function seedTicketDraft(status, priority, notes, parties) {
|
|
28
|
+
return {
|
|
29
|
+
status: (status ?? "open"),
|
|
30
|
+
priority: (priority ?? "normal"),
|
|
31
|
+
internalNotes: notes ?? "",
|
|
32
|
+
userId: parties?.userId ?? "",
|
|
33
|
+
storeId: parties?.storeId ?? "",
|
|
34
|
+
orderId: parties?.orderId ?? "",
|
|
35
|
+
productId: parties?.productId ?? "",
|
|
36
|
+
bidId: parties?.bidId ?? "",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
17
39
|
const STATUS_OPTIONS = [
|
|
18
40
|
{ label: "Open", value: "open" },
|
|
19
41
|
{ label: "In Progress", value: "in_progress" },
|
|
@@ -42,44 +64,33 @@ const ROLE_LABEL = {
|
|
|
42
64
|
export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject, userDisplayName, category, currentStatus, currentPriority, description, messages = [], internalNotes, orderId, relatedParties, statusHistory, statusHistoryTruncated, }) {
|
|
43
65
|
const queryClient = useQueryClient();
|
|
44
66
|
const { showToast } = useToast();
|
|
45
|
-
const [status, setStatus] = React.useState(currentStatus ?? "open");
|
|
46
|
-
const [priority, setPriority] = React.useState(currentPriority ?? "normal");
|
|
47
|
-
const [notes, setNotes] = React.useState(internalNotes ?? "");
|
|
48
67
|
const [replyBody, setReplyBody] = React.useState("");
|
|
49
|
-
|
|
50
|
-
const [parties, setParties] = React.useState(relatedParties ?? {});
|
|
68
|
+
const [draft, setDraft] = React.useState(() => seedTicketDraft(currentStatus, currentPriority, internalNotes, relatedParties));
|
|
51
69
|
React.useEffect(() => {
|
|
52
70
|
if (open) {
|
|
53
|
-
setStatus(currentStatus ?? "open");
|
|
54
|
-
setPriority(currentPriority ?? "normal");
|
|
55
|
-
setNotes(internalNotes ?? "");
|
|
56
71
|
setReplyBody("");
|
|
57
|
-
|
|
72
|
+
setDraft(seedTicketDraft(currentStatus, currentPriority, internalNotes, relatedParties));
|
|
58
73
|
}
|
|
59
74
|
}, [open, currentStatus, currentPriority, internalNotes, relatedParties]);
|
|
75
|
+
const sections = React.useMemo(() => buildSectionsFromSchema(supportTicketUpdateSchema, {
|
|
76
|
+
options: { status: STATUS_OPTIONS, priority: PRIORITY_OPTIONS },
|
|
77
|
+
}), []);
|
|
78
|
+
const nav = useSectionFormNav(sections, draft, { scope: "admin:support-ticket" });
|
|
79
|
+
const form = useFormShellState(supportTicketUpdateSchema, {
|
|
80
|
+
sections: nav.sectionMeta,
|
|
81
|
+
onGoToSection: nav.goToSection,
|
|
82
|
+
fieldToSectionIndex: nav.fieldToSectionIndex,
|
|
83
|
+
});
|
|
60
84
|
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["admin", "support-tickets"] });
|
|
61
|
-
const cleanedParties = React.useMemo(() => {
|
|
62
|
-
const out = {};
|
|
63
|
-
if (parties.userId?.trim())
|
|
64
|
-
out.userId = parties.userId.trim();
|
|
65
|
-
if (parties.storeId?.trim())
|
|
66
|
-
out.storeId = parties.storeId.trim();
|
|
67
|
-
if (parties.orderId?.trim())
|
|
68
|
-
out.orderId = parties.orderId.trim();
|
|
69
|
-
if (parties.productId?.trim())
|
|
70
|
-
out.productId = parties.productId.trim();
|
|
71
|
-
if (parties.bidId?.trim())
|
|
72
|
-
out.bidId = parties.bidId.trim();
|
|
73
|
-
return out;
|
|
74
|
-
}, [parties]);
|
|
75
85
|
const updateMutation = useApiMutation({
|
|
76
86
|
errorMessage: "Failed to update ticket.",
|
|
77
87
|
mutationFn: async () => {
|
|
88
|
+
const v = visibleValues(supportTicketUpdateSchema, draft);
|
|
78
89
|
await apiClient.patch(ADMIN_ENDPOINTS.SUPPORT_TICKET_BY_ID(ticketId), {
|
|
79
|
-
status,
|
|
80
|
-
priority,
|
|
81
|
-
internalNotes:
|
|
82
|
-
relatedParties:
|
|
90
|
+
status: v.status,
|
|
91
|
+
priority: v.priority,
|
|
92
|
+
internalNotes: v.internalNotes || undefined,
|
|
93
|
+
relatedParties: toRelatedPartiesPayload(v),
|
|
83
94
|
});
|
|
84
95
|
},
|
|
85
96
|
onSuccess: () => {
|
|
@@ -91,7 +102,7 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
|
|
|
91
102
|
const replyMutation = useApiMutation({
|
|
92
103
|
errorMessage: "Failed to send reply.",
|
|
93
104
|
mutationFn: async () => {
|
|
94
|
-
await apiClient.post(SUPPORT_ENDPOINTS.TICKET_MESSAGES(ticketId), { body: replyBody, newStatus: status });
|
|
105
|
+
await apiClient.post(SUPPORT_ENDPOINTS.TICKET_MESSAGES(ticketId), { body: replyBody, newStatus: draft.status });
|
|
95
106
|
},
|
|
96
107
|
onSuccess: () => {
|
|
97
108
|
showToast("Reply sent.", "success");
|
|
@@ -104,7 +115,7 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
|
|
|
104
115
|
const [storeStatus, setStoreStatus] = React.useState("active");
|
|
105
116
|
const [storeIsVerified, setStoreIsVerified] = React.useState(false);
|
|
106
117
|
const [storeIsFeatured, setStoreIsFeatured] = React.useState(false);
|
|
107
|
-
const linkedStoreId =
|
|
118
|
+
const linkedStoreId = draft.storeId?.trim();
|
|
108
119
|
const isStoreChangeRequest = category === "store_change_request";
|
|
109
120
|
// Seed the panel from the store's REAL current values, not hardcoded
|
|
110
121
|
// defaults — applyStoreChange below sends all three fields unconditionally,
|
|
@@ -140,7 +151,7 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
|
|
|
140
151
|
// Append an internal note marking the action; non-fatal if it fails.
|
|
141
152
|
apiClient
|
|
142
153
|
.patch(ADMIN_ENDPOINTS.SUPPORT_TICKET_BY_ID(ticketId), {
|
|
143
|
-
internalNotes: (
|
|
154
|
+
internalNotes: (draft.internalNotes ? draft.internalNotes + "\n" : "") +
|
|
144
155
|
`[${new Date().toISOString()}] Applied store change to ${linkedStoreId}: status=${storeStatus}, verified=${storeIsVerified}, featured=${storeIsFeatured}`,
|
|
145
156
|
})
|
|
146
157
|
.catch(console.error);
|
|
@@ -151,7 +162,7 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
|
|
|
151
162
|
// admin edit quantity per line + remove lines, then PATCHes the order).
|
|
152
163
|
const [orderItemsOpen, setOrderItemsOpen] = React.useState(false);
|
|
153
164
|
const [orderItems, setOrderItems] = React.useState([]);
|
|
154
|
-
const linkedOrderId =
|
|
165
|
+
const linkedOrderId = draft.orderId?.trim() || orderId;
|
|
155
166
|
const isOrderModificationRequest = category === "order_modification_request" || category === "order_issue";
|
|
156
167
|
const loadOrderItems = useApiMutation({
|
|
157
168
|
errorMessage: "Failed to load order.",
|
|
@@ -171,7 +182,7 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
|
|
|
171
182
|
},
|
|
172
183
|
});
|
|
173
184
|
// ST-5 — Lift Ban actions for unban_request tickets
|
|
174
|
-
const linkedUserId =
|
|
185
|
+
const linkedUserId = draft.userId?.trim();
|
|
175
186
|
const isUnbanRequest = category === "unban_request";
|
|
176
187
|
const liftHardBan = useApiMutation({
|
|
177
188
|
errorMessage: "Failed to lift hard ban.",
|
|
@@ -184,7 +195,7 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
|
|
|
184
195
|
showToast("Hard ban lifted.", "success");
|
|
185
196
|
apiClient
|
|
186
197
|
.patch(ADMIN_ENDPOINTS.SUPPORT_TICKET_BY_ID(ticketId), {
|
|
187
|
-
internalNotes: (
|
|
198
|
+
internalNotes: (draft.internalNotes ? draft.internalNotes + "\n" : "") +
|
|
188
199
|
`[${new Date().toISOString()}] Lifted hard ban for ${linkedUserId}`,
|
|
189
200
|
status: "resolved",
|
|
190
201
|
})
|
|
@@ -203,7 +214,7 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
|
|
|
203
214
|
showToast("Ticket-creation soft ban lifted.", "success");
|
|
204
215
|
apiClient
|
|
205
216
|
.patch(ADMIN_ENDPOINTS.SUPPORT_TICKET_BY_ID(ticketId), {
|
|
206
|
-
internalNotes: (
|
|
217
|
+
internalNotes: (draft.internalNotes ? draft.internalNotes + "\n" : "") +
|
|
207
218
|
`[${new Date().toISOString()}] Lifted create_support_tickets soft ban for ${linkedUserId}`,
|
|
208
219
|
status: "resolved",
|
|
209
220
|
})
|
|
@@ -235,7 +246,7 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
|
|
|
235
246
|
showToast("Order items updated.", "success");
|
|
236
247
|
apiClient
|
|
237
248
|
.patch(ADMIN_ENDPOINTS.SUPPORT_TICKET_BY_ID(ticketId), {
|
|
238
|
-
internalNotes: (
|
|
249
|
+
internalNotes: (draft.internalNotes ? draft.internalNotes + "\n" : "") +
|
|
239
250
|
`[${new Date().toISOString()}] Modified order ${linkedOrderId} items: ${orderItems
|
|
240
251
|
.filter((it) => it.quantity > 0)
|
|
241
252
|
.map((it) => `${it.productId}x${it.quantity}`)
|
|
@@ -246,13 +257,17 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
|
|
|
246
257
|
setOrderItemsOpen(false);
|
|
247
258
|
},
|
|
248
259
|
});
|
|
249
|
-
return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: subject ?? "Support Ticket", children: _jsxs(Stack, { className: `${__P.p4}`, gap: "md", children: [_jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-flex ${STATUS_COLOR[currentStatus ?? "open"] ?? STATUS_COLOR.open}`, rounded: "full", padding: "pill-sm-tall", children: (currentStatus ?? "open").replace(/_/g, " ") }), _jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: category }), orderId && (_jsxs(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: ["Order: ", orderId] }))] }), _jsxs(Div, { textSize: "sm", color: "muted", children: ["From: ", _jsx(Span, { weight: "medium", color: "primary", children: userDisplayName })] }), description && (_jsxs(Div, { className: `${__P.p3}`, rounded: "lg", surface: "muted", border: "default", children: [_jsx(Text, { className: "mb-1 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Description" }), _jsx(Text, { className: "whitespace-pre-wrap", color: "primary", size: "sm", children: description })] })), messages.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Text, { className: "tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: ["Messages (", messages.length, ")"] }), _jsx(Stack, { className: `max-h-64 ${__O.yAuto}`, gap: "sm", children: messages.map((msg, i) => (_jsxs(Div, { textSize: "sm", surface: msg.authorRole === "user" ? "default" : "info-surface", className: msg.authorRole === "user" ? "border border-[var(--appkit-color-border)]" : "border border-[var(--appkit-color-info)]", rounded: "lg", padding: "sm", children: [_jsxs(Row, { color: "muted", textSize: "xs", className: "mb-1", align: "center", gap: "sm", children: [_jsx(Span, { weight: "medium", color: "muted", children: ROLE_LABEL[msg.authorRole ?? "user"] ?? msg.authorRole }), msg.createdAt && (_jsx(Span, { children: new Date(msg.createdAt).toLocaleString() }))] }), _jsx(Text, { className: "whitespace-pre-wrap", color: "primary", children: msg.body })] }, msg.id ?? i))) })] })), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { className: "uppercase tracking-wide", color: "muted", size: "xs", weight: "semibold", children: "Reply to user" }), _jsx(Textarea, { value: replyBody, onChange: (e) => setReplyBody(e.target.value), rows: 3, placeholder: "Type a reply\u2026" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", isLoading: replyMutation.isPending, disabled: !replyBody.trim() || !ticketId || replyMutation.isPending, onClick: () => replyMutation.mutate(), children: "Send reply" })] }), _jsx(HorizontalRule, { tone: "subtle", spacing: "comfortable" }), _jsx(RecordStatusTimeline, { entries: statusHistory, truncatedCount: statusHistoryTruncated }), _jsx(HorizontalRule, { tone: "subtle", spacing: "comfortable" }),
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
260
|
+
return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: subject ?? "Support Ticket", children: _jsxs(Stack, { className: `${__P.p4}`, gap: "md", children: [_jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-flex ${STATUS_COLOR[currentStatus ?? "open"] ?? STATUS_COLOR.open}`, rounded: "full", padding: "pill-sm-tall", children: (currentStatus ?? "open").replace(/_/g, " ") }), _jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: category }), orderId && (_jsxs(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: ["Order: ", orderId] }))] }), _jsxs(Div, { textSize: "sm", color: "muted", children: ["From: ", _jsx(Span, { weight: "medium", color: "primary", children: userDisplayName })] }), description && (_jsxs(Div, { className: `${__P.p3}`, rounded: "lg", surface: "muted", border: "default", children: [_jsx(Text, { className: "mb-1 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Description" }), _jsx(Text, { className: "whitespace-pre-wrap", color: "primary", size: "sm", children: description })] })), messages.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Text, { className: "tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: ["Messages (", messages.length, ")"] }), _jsx(Stack, { className: `max-h-64 ${__O.yAuto}`, gap: "sm", children: messages.map((msg, i) => (_jsxs(Div, { textSize: "sm", surface: msg.authorRole === "user" ? "default" : "info-surface", className: msg.authorRole === "user" ? "border border-[var(--appkit-color-border)]" : "border border-[var(--appkit-color-info)]", rounded: "lg", padding: "sm", children: [_jsxs(Row, { color: "muted", textSize: "xs", className: "mb-1", align: "center", gap: "sm", children: [_jsx(Span, { weight: "medium", color: "muted", children: ROLE_LABEL[msg.authorRole ?? "user"] ?? msg.authorRole }), msg.createdAt && (_jsx(Span, { children: new Date(msg.createdAt).toLocaleString() }))] }), _jsx(Text, { className: "whitespace-pre-wrap", color: "primary", children: msg.body })] }, msg.id ?? i))) })] })), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { className: "uppercase tracking-wide", color: "muted", size: "xs", weight: "semibold", children: "Reply to user" }), _jsx(Textarea, { value: replyBody, onChange: (e) => setReplyBody(e.target.value), rows: 3, placeholder: "Type a reply\u2026" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", isLoading: replyMutation.isPending, disabled: !replyBody.trim() || !ticketId || replyMutation.isPending, onClick: () => replyMutation.mutate(), children: "Send reply" })] }), _jsx(HorizontalRule, { tone: "subtle", spacing: "comfortable" }), _jsx(RecordStatusTimeline, { entries: statusHistory, truncatedCount: statusHistoryTruncated }), _jsx(HorizontalRule, { tone: "subtle", spacing: "comfortable" }), _jsxs(FormShellContext.Provider, { value: form.shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: draft, onChange: (partial) => setDraft((d) => ({ ...d, ...partial })), onSubmit: () => {
|
|
261
|
+
// SectionForm scrolls to the first erroring section and then
|
|
262
|
+
// submits regardless, so the guard lives here.
|
|
263
|
+
form.clearErrors();
|
|
264
|
+
const parsed = supportTicketUpdateSchema.safeParse(visibleValues(supportTicketUpdateSchema, draft));
|
|
265
|
+
if (!parsed.success) {
|
|
266
|
+
applyZodIssues(parsed.error.issues, form.setFieldError);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
updateMutation.mutate();
|
|
270
|
+
}, onValidationChange: () => form.validate(draft), schema: supportTicketUpdateSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, submitLabel: "Save changes", cancelLabel: "Cancel", onCancel: onClose, isLoading: updateMutation.isPending })] }), isOrderModificationRequest && (_jsx(OrderItemsPanel, { linkedOrderId: linkedOrderId, orderItemsOpen: orderItemsOpen, setOrderItemsOpen: setOrderItemsOpen, orderItems: orderItems, setOrderItems: setOrderItems, loadOrderItems: loadOrderItems, applyOrderItems: applyOrderItems })), isStoreChangeRequest && (_jsx(StoreChangePanel, { linkedStoreId: linkedStoreId, storeStatus: storeStatus, setStoreStatus: setStoreStatus, storeIsVerified: storeIsVerified, setStoreIsVerified: setStoreIsVerified, storeIsFeatured: storeIsFeatured, setStoreIsFeatured: setStoreIsFeatured, applyStoreChange: applyStoreChange, isLoadingStore: storeQuery.isLoading })), isUnbanRequest && (_jsx(UnbanRequestPanel, { linkedUserId: linkedUserId, liftHardBan: liftHardBan, liftSoftBanTickets: liftSoftBanTickets }))] }) }));
|
|
256
271
|
}
|
|
257
272
|
function OrderItemsPanel(props) {
|
|
258
273
|
const { linkedOrderId, orderItemsOpen, setOrderItemsOpen, orderItems, setOrderItems, loadOrderItems, applyOrderItems, } = props;
|
|
@@ -3,10 +3,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { Code, useApiMutation } from "@mohasinac/appkit/client";
|
|
4
4
|
import React from "react";
|
|
5
5
|
import { useQueryClient } from "@tanstack/react-query";
|
|
6
|
-
import { Avatar, Button, ConfirmDeleteModal, Div,
|
|
6
|
+
import { Avatar, Button, ConfirmDeleteModal, Div, Heading, Input, Row, Select, SideDrawer, Span, Stack, StackedViewShell, Text, Textarea, useToast } from "../../../ui";
|
|
7
7
|
import { apiClient } from "../../../http";
|
|
8
8
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
9
|
-
import { adminUserUpdateSchema } from "../schemas/admin-user-form";
|
|
9
|
+
import { adminUserUpdateSchema, toPublicProfilePayload, } from "../schemas/admin-user-form";
|
|
10
|
+
import { SectionForm, useSectionFormNav, buildSectionsFromSchema, visibleValues, } from "../../shell";
|
|
11
|
+
import { useFormShellState, FormShellContext } from "../../../ui/forms";
|
|
12
|
+
import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
|
|
10
13
|
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
11
14
|
const ROLE_OPTIONS = [
|
|
12
15
|
{ label: "User (buyer)", value: "user" },
|
|
@@ -47,22 +50,9 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
|
|
|
47
50
|
const queryClient = useQueryClient();
|
|
48
51
|
const { showToast } = useToast();
|
|
49
52
|
// --- General fields -------------------------------------------------------
|
|
50
|
-
const [
|
|
51
|
-
const [emailVerified, setEmailVerified] = React.useState(currentEmailVerified ?? false);
|
|
52
|
-
const [isTester, setIsTester] = React.useState(currentIsTester ?? false);
|
|
53
|
-
const [canTestAdmin, setCanTestAdmin] = React.useState(currentCanTestAdmin ?? false);
|
|
54
|
-
const [adminNotes, setAdminNotes] = React.useState("");
|
|
53
|
+
const [draft, setDraft] = React.useState({});
|
|
55
54
|
const [deleteOpen, setDeleteOpen] = React.useState(false);
|
|
56
55
|
// --- ST-2 profile fields --------------------------------------------------
|
|
57
|
-
const [editDisplayName, setEditDisplayName] = React.useState(displayName ?? "");
|
|
58
|
-
const [phoneNumber, setPhoneNumber] = React.useState(currentPhoneNumber ?? "");
|
|
59
|
-
const [bio, setBio] = React.useState(currentBio ?? "");
|
|
60
|
-
const [location, setLocation] = React.useState(currentLocation ?? "");
|
|
61
|
-
const [website, setWebsite] = React.useState(currentWebsite ?? "");
|
|
62
|
-
const [twitter, setTwitter] = React.useState(currentSocialLinks?.twitter ?? "");
|
|
63
|
-
const [instagram, setInstagram] = React.useState(currentSocialLinks?.instagram ?? "");
|
|
64
|
-
const [facebook, setFacebook] = React.useState(currentSocialLinks?.facebook ?? "");
|
|
65
|
-
const [linkedin, setLinkedin] = React.useState(currentSocialLinks?.linkedin ?? "");
|
|
66
56
|
// --- Hard ban form --------------------------------------------------------
|
|
67
57
|
const [showHardBanForm, setShowHardBanForm] = React.useState(false);
|
|
68
58
|
const [hardBanReasonInput, setHardBanReasonInput] = React.useState("");
|
|
@@ -73,20 +63,22 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
|
|
|
73
63
|
const [softBanExpiry, setSoftBanExpiry] = React.useState("");
|
|
74
64
|
React.useEffect(() => {
|
|
75
65
|
if (open) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
66
|
+
setDraft({
|
|
67
|
+
role: (currentRole ?? "user"),
|
|
68
|
+
emailVerified: currentEmailVerified ?? false,
|
|
69
|
+
isTester: currentIsTester ?? false,
|
|
70
|
+
canTestAdmin: currentCanTestAdmin ?? false,
|
|
71
|
+
adminNotes: "",
|
|
72
|
+
displayName: displayName ?? "",
|
|
73
|
+
phoneNumber: currentPhoneNumber ?? "",
|
|
74
|
+
bio: currentBio ?? "",
|
|
75
|
+
location: currentLocation ?? "",
|
|
76
|
+
website: currentWebsite ?? "",
|
|
77
|
+
twitter: currentSocialLinks?.twitter ?? "",
|
|
78
|
+
instagram: currentSocialLinks?.instagram ?? "",
|
|
79
|
+
facebook: currentSocialLinks?.facebook ?? "",
|
|
80
|
+
linkedin: currentSocialLinks?.linkedin ?? "",
|
|
81
|
+
});
|
|
90
82
|
setShowHardBanForm(false);
|
|
91
83
|
setHardBanReasonInput("");
|
|
92
84
|
setShowAddSoftBan(false);
|
|
@@ -110,40 +102,34 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
|
|
|
110
102
|
currentSocialLinks?.facebook,
|
|
111
103
|
currentSocialLinks?.linkedin,
|
|
112
104
|
]);
|
|
105
|
+
const sections = React.useMemo(() => buildSectionsFromSchema(adminUserUpdateSchema, {
|
|
106
|
+
options: { role: ROLE_OPTIONS },
|
|
107
|
+
}), []);
|
|
108
|
+
const nav = useSectionFormNav(sections, draft, { scope: "admin:user-editor" });
|
|
109
|
+
const form = useFormShellState(adminUserUpdateSchema, {
|
|
110
|
+
sections: nav.sectionMeta,
|
|
111
|
+
onGoToSection: nav.goToSection,
|
|
112
|
+
fieldToSectionIndex: nav.fieldToSectionIndex,
|
|
113
|
+
});
|
|
113
114
|
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["admin", "users"] });
|
|
114
115
|
// --- Mutations ------------------------------------------------------------
|
|
115
116
|
const saveMutation = useApiMutation({
|
|
116
117
|
errorMessage: "Failed to update user.",
|
|
117
118
|
mutationFn: async () => {
|
|
118
|
-
|
|
119
|
-
// the PATCH payload minimal and avoids overwriting unrelated subkeys.
|
|
120
|
-
const socialLinks = {};
|
|
121
|
-
if (twitter.trim())
|
|
122
|
-
socialLinks.twitter = twitter.trim();
|
|
123
|
-
if (instagram.trim())
|
|
124
|
-
socialLinks.instagram = instagram.trim();
|
|
125
|
-
if (facebook.trim())
|
|
126
|
-
socialLinks.facebook = facebook.trim();
|
|
127
|
-
if (linkedin.trim())
|
|
128
|
-
socialLinks.linkedin = linkedin.trim();
|
|
129
|
-
const publicProfile = {};
|
|
130
|
-
if (bio.trim())
|
|
131
|
-
publicProfile.bio = bio.trim();
|
|
132
|
-
if (location.trim())
|
|
133
|
-
publicProfile.location = location.trim();
|
|
134
|
-
if (website.trim())
|
|
135
|
-
publicProfile.website = website.trim();
|
|
136
|
-
if (Object.keys(socialLinks).length > 0)
|
|
137
|
-
publicProfile.socialLinks = socialLinks;
|
|
119
|
+
const v = visibleValues(adminUserUpdateSchema, draft);
|
|
138
120
|
await apiClient.patch(ADMIN_ENDPOINTS.USER_BY_ID(userId), {
|
|
139
|
-
role,
|
|
140
|
-
emailVerified,
|
|
141
|
-
isTester,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
121
|
+
role: v.role,
|
|
122
|
+
emailVerified: v.emailVerified,
|
|
123
|
+
isTester: v.isTester,
|
|
124
|
+
// `when` already drops this once `isTester` is off; the `?? false` is
|
|
125
|
+
// what CLEARS a previously-granted flag rather than omitting the key.
|
|
126
|
+
canTestAdmin: v.canTestAdmin ?? false,
|
|
127
|
+
adminNotes: v.adminNotes || undefined,
|
|
128
|
+
displayName: v.displayName?.trim() || undefined,
|
|
129
|
+
phoneNumber: v.phoneNumber?.trim() || undefined,
|
|
130
|
+
// ST-2 — a partial: only the subkeys that were filled in, so an
|
|
131
|
+
// untouched field never overwrites what is stored.
|
|
132
|
+
publicProfile: toPublicProfilePayload(v),
|
|
147
133
|
});
|
|
148
134
|
},
|
|
149
135
|
onSuccess: () => {
|
|
@@ -216,29 +202,26 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
|
|
|
216
202
|
const softBans = currentSoftBans ?? [];
|
|
217
203
|
const renderAvatarHeader = () => userId ? (_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(Avatar, { src: photoURL ?? null, name: displayName ?? userId, size: "md" }), _jsx(Text, { size: "sm", weight: "semibold", children: displayName ?? userId })] })) : null;
|
|
218
204
|
const renderInfoCard = () => userId ? (_jsx(Div, { textSize: "xs", surface: "muted", rounded: "lg", border: "default", padding: "inlineSm", children: _jsxs(Stack, { color: "primary", gap: "xs", children: [_jsxs(Text, { size: "xs", children: [_jsx(Span, { weight: "semibold", children: "Owner ID (Firebase UID):" }), " ", _jsx(Code, { className: "select-all font-mono", children: userId })] }), ownedStoreId && (_jsxs(Text, { size: "xs", children: [_jsx(Span, { weight: "semibold", children: "Owns store:" }), " ", _jsx(Code, { className: "select-all font-mono", children: ownedStoreId }), ownedStoreName ? ` — ${ownedStoreName}` : ""] }))] }) })) : null;
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
205
|
+
const renderFieldsSection = () => (_jsx(SectionForm, { sections: sections, values: draft, onChange: (partial) => setDraft((d) => ({ ...d, ...partial })), onSubmit: () => {
|
|
206
|
+
// SectionForm scrolls to the first erroring section and then submits
|
|
207
|
+
// regardless, so the guard lives here.
|
|
208
|
+
form.clearErrors();
|
|
209
|
+
const parsed = adminUserUpdateSchema.safeParse(visibleValues(adminUserUpdateSchema, draft));
|
|
210
|
+
if (!parsed.success) {
|
|
211
|
+
applyZodIssues(parsed.error.issues, form.setFieldError);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
saveMutation.mutate();
|
|
215
|
+
}, onValidationChange: () => form.validate(draft), schema: adminUserUpdateSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, submitLabel: "Save changes", cancelLabel: "Cancel", onCancel: onClose, isLoading: saveMutation.isPending, destructiveAction: {
|
|
216
|
+
label: "Delete user",
|
|
217
|
+
onClick: () => setDeleteOpen(true),
|
|
218
|
+
disabled: !userId,
|
|
219
|
+
} }));
|
|
229
220
|
const renderModerationSection = () => (_jsxs(Stack, { border: "default", as: "section", gap: "sm", className: "border-t", padding: "t-md", children: [_jsx(Heading, { level: 3, className: "mb-3", color: "muted", size: "sm", weight: "semibold", children: "Moderation" }), _jsx(HardBanPanel, { userId: userId, isHardBanned: isHardBanned, currentHardBanReason: currentHardBanReason, showHardBanForm: showHardBanForm, setShowHardBanForm: setShowHardBanForm, hardBanReasonInput: hardBanReasonInput, setHardBanReasonInput: setHardBanReasonInput, hardBanPending: hardBanMutation.isPending, unbanPending: unbanMutation.isPending, onHardBan: (reason) => hardBanMutation.mutate(reason), onUnban: () => unbanMutation.mutate() }), _jsx(SoftBanPanel, { userId: userId, softBans: softBans, showAddSoftBan: showAddSoftBan, setShowAddSoftBan: setShowAddSoftBan, softBanAction: softBanAction, setSoftBanAction: setSoftBanAction, softBanReason: softBanReason, setSoftBanReason: setSoftBanReason, softBanExpiry: softBanExpiry, setSoftBanExpiry: setSoftBanExpiry, softBanPending: softBanMutation.isPending, liftPending: liftSoftBanMutation.isPending, onAddSoftBan: (payload) => softBanMutation.mutate(payload), onLiftSoftBan: (action) => liftSoftBanMutation.mutate(action) })] }));
|
|
230
|
-
return (_jsxs(_Fragment, { children: [_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: displayName ? `Manage: ${displayName}` : "Manage User", children: _jsxs(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
renderRoleSection,
|
|
237
|
-
renderEmailVerifiedSection,
|
|
238
|
-
renderIsTesterSection,
|
|
239
|
-
renderProfileSection,
|
|
240
|
-
renderAdminNotesSection,
|
|
241
|
-
renderActionsSection,
|
|
242
|
-
renderModerationSection,
|
|
243
|
-
] })] }) }), _jsx(ConfirmDeleteModal, { isOpen: deleteOpen, onClose: () => setDeleteOpen(false), onConfirm: () => deleteMutation.mutate(), isDeleting: deleteMutation.isPending, title: `Delete ${displayName ?? "user"}?`, message: "This action cannot be undone. The user's account and all associated data will be permanently removed.", confirmText: "Delete user", variant: "danger" })] }));
|
|
221
|
+
return (_jsxs(_Fragment, { children: [_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: displayName ? `Manage: ${displayName}` : "Manage User", children: _jsx(FormShellContext.Provider, { value: form.shellCtx, children: _jsxs(Div, { padding: "md", children: [_jsx(FormErrorSummary, {}), _jsx(StackedViewShell, { portal: "admin", className: "space-y-4", sections: [
|
|
222
|
+
renderAvatarHeader,
|
|
223
|
+
renderInfoCard,
|
|
224
|
+
renderFieldsSection,
|
|
225
|
+
renderModerationSection,
|
|
226
|
+
] })] }) }) }), _jsx(ConfirmDeleteModal, { isOpen: deleteOpen, onClose: () => setDeleteOpen(false), onConfirm: () => deleteMutation.mutate(), isDeleting: deleteMutation.isPending, title: `Delete ${displayName ?? "user"}?`, message: "This action cannot be undone. The user's account and all associated data will be permanently removed.", confirmText: "Delete user", variant: "danger" })] }));
|
|
244
227
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface AdminPageTab {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
description: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Roles, and the permission catalogue you consult while building one.
|
|
8
|
+
*
|
|
9
|
+
* The catalogue is a REFERENCE SHEET, not a second editor: it lists what each
|
|
10
|
+
* permission string grants. Reading it is something you do mid-task, with the
|
|
11
|
+
* role you are editing still on screen — which is the argument for a tab
|
|
12
|
+
* rather than a separate page.
|
|
13
|
+
*/
|
|
14
|
+
export declare const ROLES_TABS: readonly [{
|
|
15
|
+
readonly id: "roles";
|
|
16
|
+
readonly label: "Roles";
|
|
17
|
+
readonly description: "Who can do what, as named roles.";
|
|
18
|
+
}, {
|
|
19
|
+
readonly id: "permissions";
|
|
20
|
+
readonly label: "Permission catalogue";
|
|
21
|
+
readonly description: "Every permission string and what it grants.";
|
|
22
|
+
}];
|
|
23
|
+
export type RolesTabId = (typeof ROLES_TABS)[number]["id"];
|
|
24
|
+
export declare function isRolesTabId(value: string | null | undefined): value is RolesTabId;
|
|
25
|
+
/** Both toggle platform surfaces on and off, into the same settings document. */
|
|
26
|
+
export declare const ADMIN_SETTINGS_TABS: readonly [{
|
|
27
|
+
readonly id: "actions";
|
|
28
|
+
readonly label: "Actions";
|
|
29
|
+
readonly description: "Turn individual CTAs and bulk actions on or off.";
|
|
30
|
+
}, {
|
|
31
|
+
readonly id: "navigation";
|
|
32
|
+
readonly label: "Navigation";
|
|
33
|
+
readonly description: "Turn nav entries on or off across the portals.";
|
|
34
|
+
}];
|
|
35
|
+
export type AdminSettingsTabId = (typeof ADMIN_SETTINGS_TABS)[number]["id"];
|
|
36
|
+
export declare function isAdminSettingsTabId(value: string | null | undefined): value is AdminSettingsTabId;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: The admin half of W8's C2. Two consolidations, each between pages that
|
|
3
|
+
* already share one permission — which is the test that decides whether
|
|
4
|
+
* two pages CAN be tabs at all: one page has one `requiredPermission`, so
|
|
5
|
+
* merging across a permission line either widens or narrows access
|
|
6
|
+
* silently.
|
|
7
|
+
*
|
|
8
|
+
* `roles` and `permissions` are both `admin:roles:read`; `settings/actions`
|
|
9
|
+
* and `settings/navigation` are both `admin:settings:write`. Pairs that
|
|
10
|
+
* failed that test were left alone — see the plan's "deliberately NOT
|
|
11
|
+
* consolidated" list.
|
|
12
|
+
*
|
|
13
|
+
* EXPORTS:
|
|
14
|
+
* ROLES_TABS / isRolesTabId, ADMIN_SETTINGS_TABS / isAdminSettingsTabId
|
|
15
|
+
*
|
|
16
|
+
* @tag domain:admin
|
|
17
|
+
* @tag layer:constants
|
|
18
|
+
* @tag pattern:registry
|
|
19
|
+
* @tag access:isomorphic
|
|
20
|
+
* @tag consumers:admin roles + settings/actions pages,action index
|
|
21
|
+
* @tag sideEffects:none
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Roles, and the permission catalogue you consult while building one.
|
|
25
|
+
*
|
|
26
|
+
* The catalogue is a REFERENCE SHEET, not a second editor: it lists what each
|
|
27
|
+
* permission string grants. Reading it is something you do mid-task, with the
|
|
28
|
+
* role you are editing still on screen — which is the argument for a tab
|
|
29
|
+
* rather than a separate page.
|
|
30
|
+
*/
|
|
31
|
+
export const ROLES_TABS = [
|
|
32
|
+
{ id: "roles", label: "Roles", description: "Who can do what, as named roles." },
|
|
33
|
+
{ id: "permissions", label: "Permission catalogue", description: "Every permission string and what it grants." },
|
|
34
|
+
];
|
|
35
|
+
export function isRolesTabId(value) {
|
|
36
|
+
return !!value && ROLES_TABS.some((t) => t.id === value);
|
|
37
|
+
}
|
|
38
|
+
/** Both toggle platform surfaces on and off, into the same settings document. */
|
|
39
|
+
export const ADMIN_SETTINGS_TABS = [
|
|
40
|
+
{ id: "actions", label: "Actions", description: "Turn individual CTAs and bulk actions on or off." },
|
|
41
|
+
{ id: "navigation", label: "Navigation", description: "Turn nav entries on or off across the portals." },
|
|
42
|
+
];
|
|
43
|
+
export function isAdminSettingsTabId(value) {
|
|
44
|
+
return !!value && ADMIN_SETTINGS_TABS.some((t) => t.id === value);
|
|
45
|
+
}
|
|
@@ -173,3 +173,84 @@ export declare const adminAdFormSchema: z.ZodEffects<z.ZodObject<{
|
|
|
173
173
|
thirdPartyUrl?: string | undefined;
|
|
174
174
|
}>;
|
|
175
175
|
export type AdminAdFormValues = z.infer<typeof adminAdFormSchema>;
|
|
176
|
+
/**
|
|
177
|
+
* Ad SETTINGS — the provider credentials and the global consent switch.
|
|
178
|
+
*
|
|
179
|
+
* Distinct from `adminAdFormSchema`, which is one ad record. This is the
|
|
180
|
+
* platform-level configuration every ad renders under, and it was validated by
|
|
181
|
+
* a hand-written `localCredentialIssues` array that computed the same two rules
|
|
182
|
+
* on every keystroke and rendered them as a banner headed "Fix settings before
|
|
183
|
+
* saving" — never on the field that was wrong.
|
|
184
|
+
*
|
|
185
|
+
* Both credentials are optional and BLANK MEANS UNCHANGED: the route stores
|
|
186
|
+
* them encrypted and returns only a masked form, so the input starts empty on
|
|
187
|
+
* every load and an empty submit must leave the stored value alone. That is why
|
|
188
|
+
* neither field has a `.min()` — an empty string here is not an omission.
|
|
189
|
+
*/
|
|
190
|
+
export declare const adsConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
191
|
+
consentRequired: z.ZodBoolean;
|
|
192
|
+
adsenseClientId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
193
|
+
thirdPartyScriptUrl: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
194
|
+
}, "strip", z.ZodTypeAny, {
|
|
195
|
+
consentRequired: boolean;
|
|
196
|
+
adsenseClientId?: string | undefined;
|
|
197
|
+
thirdPartyScriptUrl?: string | undefined;
|
|
198
|
+
}, {
|
|
199
|
+
consentRequired: boolean;
|
|
200
|
+
adsenseClientId?: string | undefined;
|
|
201
|
+
thirdPartyScriptUrl?: string | undefined;
|
|
202
|
+
}>, {
|
|
203
|
+
consentRequired: boolean;
|
|
204
|
+
adsenseClientId?: string | undefined;
|
|
205
|
+
thirdPartyScriptUrl?: string | undefined;
|
|
206
|
+
}, {
|
|
207
|
+
consentRequired: boolean;
|
|
208
|
+
adsenseClientId?: string | undefined;
|
|
209
|
+
thirdPartyScriptUrl?: string | undefined;
|
|
210
|
+
}>;
|
|
211
|
+
export type AdsConfigValues = z.infer<typeof adsConfigSchema>;
|
|
212
|
+
/**
|
|
213
|
+
* The support-ticket triage form — what an admin CHANGES about a ticket, as
|
|
214
|
+
* distinct from what they SAY on it (the reply box, which is its own write to
|
|
215
|
+
* a different endpoint and carries the ticket's new status with it).
|
|
216
|
+
*
|
|
217
|
+
* The five linked-party ids are FLAT here and reassembled into
|
|
218
|
+
* `relatedParties` at the payload boundary, the same call the ad creative and
|
|
219
|
+
* the user's public profile make: the stored shape is one nested object, and
|
|
220
|
+
* the form has always offered five discrete inputs.
|
|
221
|
+
*/
|
|
222
|
+
export declare const supportTicketUpdateSchema: z.ZodObject<{
|
|
223
|
+
status: z.ZodEnum<["open", "in_progress", "waiting_on_user", "resolved", "closed"]>;
|
|
224
|
+
priority: z.ZodEnum<["low", "normal", "high", "urgent"]>;
|
|
225
|
+
internalNotes: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
226
|
+
userId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
227
|
+
storeId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
228
|
+
orderId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
229
|
+
productId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
230
|
+
bidId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
231
|
+
}, "strip", z.ZodTypeAny, {
|
|
232
|
+
status: "closed" | "open" | "resolved" | "in_progress" | "waiting_on_user";
|
|
233
|
+
priority: "normal" | "high" | "low" | "urgent";
|
|
234
|
+
storeId?: string | undefined;
|
|
235
|
+
userId?: string | undefined;
|
|
236
|
+
orderId?: string | undefined;
|
|
237
|
+
productId?: string | undefined;
|
|
238
|
+
bidId?: string | undefined;
|
|
239
|
+
internalNotes?: string | undefined;
|
|
240
|
+
}, {
|
|
241
|
+
status: "closed" | "open" | "resolved" | "in_progress" | "waiting_on_user";
|
|
242
|
+
priority: "normal" | "high" | "low" | "urgent";
|
|
243
|
+
storeId?: string | undefined;
|
|
244
|
+
userId?: string | undefined;
|
|
245
|
+
orderId?: string | undefined;
|
|
246
|
+
productId?: string | undefined;
|
|
247
|
+
bidId?: string | undefined;
|
|
248
|
+
internalNotes?: string | undefined;
|
|
249
|
+
}>;
|
|
250
|
+
export type SupportTicketUpdateValues = z.infer<typeof supportTicketUpdateSchema>;
|
|
251
|
+
/**
|
|
252
|
+
* The five flat id fields, back into the nested `relatedParties` the ticket
|
|
253
|
+
* stores. Blank fields are dropped, and an entirely empty set returns
|
|
254
|
+
* `undefined` so the key is omitted rather than written as `{}`.
|
|
255
|
+
*/
|
|
256
|
+
export declare function toRelatedPartiesPayload(v: SupportTicketUpdateValues): Record<string, string> | undefined;
|