@mohasinac/appkit 4.31.1 → 4.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/account/components/UserBidsView.js +6 -1
- package/dist/features/account/components/UserOrdersView.js +8 -1
- package/dist/features/account/components/UserReturnsView.js +6 -1
- package/dist/features/admin/components/AdminAdsView.js +36 -34
- package/dist/features/admin/components/AdminAuditLogView.js +12 -1
- package/dist/features/admin/components/AdminBundlesView.js +5 -1
- package/dist/features/admin/components/AdminCarouselEditorView.js +78 -10
- package/dist/features/admin/components/AdminFaqsView.js +10 -1
- package/dist/features/admin/components/AdminNotificationsView.js +7 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +55 -24
- package/dist/features/admin/components/AdminPrizeDrawsView.js +11 -1
- 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/AdminStoresView.js +13 -1
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +58 -43
- package/dist/features/admin/components/AdminSupportTicketsView.js +11 -1
- package/dist/features/admin/components/AdminTeamView.js +11 -1
- package/dist/features/admin/components/AdminTesterChecklistView.js +6 -1
- package/dist/features/admin/components/AdminUserEditorView.js +65 -82
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.js +6 -1
- 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/SellerBundlesView.js +5 -1
- package/dist/features/seller/components/SellerGoogleReviewsView.js +31 -19
- package/dist/features/seller/components/SellerOrdersView.js +57 -19
- package/dist/features/seller/components/SellerProductsView.js +10 -1
- 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/stores/actions/store-query-actions.js +5 -3
- package/dist/features/stores/repository/store.repository.d.ts +3 -1
- package/dist/features/stores/repository/store.repository.js +6 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +79 -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 +2 -0
- package/dist/next/index.js +1 -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
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize";
|
|
4
|
-
import { useCallback, useState } from "react";
|
|
4
|
+
import React, { useCallback, useState } from "react";
|
|
5
|
+
import { reviewReplySchema, reviewBulkReplySchema, reviewContestSchema, reviewFeedbackSchema, } from "../schemas/review-forms";
|
|
6
|
+
import { SectionForm, useSectionFormNav, buildSectionsFromSchema, visibleValues, } from "../../shell";
|
|
7
|
+
import { useFormShellState, FormShellContext } from "../../../ui/forms";
|
|
8
|
+
import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
|
|
9
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
5
10
|
import { useQueryClient } from "@tanstack/react-query";
|
|
6
|
-
import { Alert, Badge, Button, Checkbox, Div, Modal, Row, Select, SideDrawer, Span, Stack, Text,
|
|
11
|
+
import { Alert, Badge, Button, Checkbox, Div, Modal, Row, Select, SideDrawer, Span, Stack, Text, useToast } from "../../../ui";
|
|
7
12
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
8
13
|
import { SELLER_BULK_ACTIONS, ROW_ACTION_META } from "../../products/constants/action-defs";
|
|
9
14
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
@@ -32,28 +37,58 @@ function statusBadge(status) {
|
|
|
32
37
|
};
|
|
33
38
|
return _jsx(Badge, { variant: map[status] ?? "default", children: status });
|
|
34
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The four write surfaces on this page are each ONE textarea, and four
|
|
42
|
+
* copies of the same twenty lines of SectionForm wiring is exactly the Rule of
|
|
43
|
+
* Three trigger. It stays local: the shape is "a single-field modal form", not
|
|
44
|
+
* a generally useful primitive, and `QuickFormDrawer` already owns the
|
|
45
|
+
* generalised version for drawers.
|
|
46
|
+
*
|
|
47
|
+
* `hideActions` is deliberately NOT used — each modal's own footer buttons are
|
|
48
|
+
* removed in favour of this row, so there is one Save and one Cancel per form.
|
|
49
|
+
*/
|
|
50
|
+
function ReviewTextForm({ schema, values, onChange, onSubmit, submitLabel, onCancel, isLoading, scope, }) {
|
|
51
|
+
const sections = React.useMemo(() => buildSectionsFromSchema(schema), [schema]);
|
|
52
|
+
const nav = useSectionFormNav(sections, values, { scope });
|
|
53
|
+
const form = useFormShellState(schema, {
|
|
54
|
+
sections: nav.sectionMeta,
|
|
55
|
+
onGoToSection: nav.goToSection,
|
|
56
|
+
fieldToSectionIndex: nav.fieldToSectionIndex,
|
|
57
|
+
});
|
|
58
|
+
return (_jsxs(FormShellContext.Provider, { value: form.shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: values, onChange: onChange, onSubmit: () => {
|
|
59
|
+
// SectionForm scrolls to the first erroring section and then submits
|
|
60
|
+
// regardless, so the guard lives here.
|
|
61
|
+
form.clearErrors();
|
|
62
|
+
const parsed = schema.safeParse(visibleValues(schema, values));
|
|
63
|
+
if (!parsed.success) {
|
|
64
|
+
applyZodIssues(parsed.error.issues, form.setFieldError);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
onSubmit();
|
|
68
|
+
}, onValidationChange: () => form.validate(values), schema: schema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, submitLabel: submitLabel, cancelLabel: "Cancel", onCancel: onCancel, isLoading: isLoading })] }));
|
|
69
|
+
}
|
|
35
70
|
export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, replyApiBase = SELLER_ENDPOINTS.REVIEWS, }) {
|
|
36
71
|
const queryClient = useQueryClient();
|
|
37
72
|
const { showToast } = useToast();
|
|
38
73
|
const refetchReviews = useCallback(() => queryClient.invalidateQueries({ queryKey: ["seller", "reviews", "listing"] }), [queryClient]);
|
|
39
74
|
// Reply drawer
|
|
40
75
|
const [replyTarget, setReplyTarget] = useState(null);
|
|
41
|
-
const [
|
|
76
|
+
const [replyDraft, setReplyDraft] = useState({ reply: "" });
|
|
42
77
|
const [replySaving, setReplySaving] = useState(false);
|
|
43
78
|
const [replyError, setReplyError] = useState(null);
|
|
44
79
|
// S-STORE-4-C — bulk reply + contest + feedback selection
|
|
45
80
|
const [bulkReplyOpen, setBulkReplyOpen] = useState(false);
|
|
46
|
-
const [
|
|
81
|
+
const [bulkDraft, setBulkDraft] = useState({ reply: "" });
|
|
47
82
|
const [bulkSaving, setBulkSaving] = useState(false);
|
|
48
83
|
const [bulkTargetIds, setBulkTargetIds] = useState([]);
|
|
49
84
|
const [bulkClearSelection, setBulkClearSelection] = useState(null);
|
|
50
85
|
const [contestTarget, setContestTarget] = useState(null);
|
|
51
|
-
const [
|
|
86
|
+
const [contestDraft, setContestDraft] = useState({ reason: "" });
|
|
52
87
|
const [feedbackTarget, setFeedbackTarget] = useState(null);
|
|
53
|
-
const [
|
|
88
|
+
const [feedbackDraft, setFeedbackDraft] = useState({ feedback: "" });
|
|
54
89
|
const openReply = (review) => {
|
|
55
90
|
setReplyTarget(review);
|
|
56
|
-
|
|
91
|
+
setReplyDraft({ reply: review.sellerReply ?? "" });
|
|
57
92
|
setReplyError(null);
|
|
58
93
|
};
|
|
59
94
|
const handleReplySave = async (onDone) => {
|
|
@@ -65,7 +100,7 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
65
100
|
const res = await fetch(`${replyApiBase}/${replyTarget.id}/reply`, {
|
|
66
101
|
method: "POST",
|
|
67
102
|
headers: { "Content-Type": "application/json" },
|
|
68
|
-
body: JSON.stringify({ reply:
|
|
103
|
+
body: JSON.stringify({ reply: replyDraft.reply }),
|
|
69
104
|
});
|
|
70
105
|
const json = await res.json();
|
|
71
106
|
if (!res.ok)
|
|
@@ -91,7 +126,7 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
91
126
|
const res = await fetch(SELLER_ENDPOINTS.REVIEW_CONTEST(contestTarget.id), {
|
|
92
127
|
method: "POST",
|
|
93
128
|
headers: { "Content-Type": "application/json" },
|
|
94
|
-
body: JSON.stringify({ reason:
|
|
129
|
+
body: JSON.stringify({ reason: contestDraft.reason }),
|
|
95
130
|
});
|
|
96
131
|
if (!res.ok)
|
|
97
132
|
throw new Error(`Could not submit the contest (${res.status})`);
|
|
@@ -101,9 +136,9 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
101
136
|
return;
|
|
102
137
|
}
|
|
103
138
|
setContestTarget(null);
|
|
104
|
-
|
|
139
|
+
setContestDraft({ reason: "" });
|
|
105
140
|
onDone();
|
|
106
|
-
}, [contestTarget,
|
|
141
|
+
}, [contestTarget, contestDraft, showToast]);
|
|
107
142
|
const submitFeedback = useCallback(async () => {
|
|
108
143
|
if (!feedbackTarget)
|
|
109
144
|
return;
|
|
@@ -111,7 +146,7 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
111
146
|
const res = await fetch(SELLER_ENDPOINTS.REVIEW_FEEDBACK(feedbackTarget.id), {
|
|
112
147
|
method: "POST",
|
|
113
148
|
headers: { "Content-Type": "application/json" },
|
|
114
|
-
body: JSON.stringify({ feedback:
|
|
149
|
+
body: JSON.stringify({ feedback: feedbackDraft.feedback }),
|
|
115
150
|
});
|
|
116
151
|
if (!res.ok)
|
|
117
152
|
throw new Error(`Could not send the feedback (${res.status})`);
|
|
@@ -121,10 +156,10 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
121
156
|
return;
|
|
122
157
|
}
|
|
123
158
|
setFeedbackTarget(null);
|
|
124
|
-
|
|
125
|
-
}, [feedbackTarget,
|
|
159
|
+
setFeedbackDraft({ feedback: "" });
|
|
160
|
+
}, [feedbackTarget, feedbackDraft, showToast]);
|
|
126
161
|
const submitBulkReply = useCallback(async () => {
|
|
127
|
-
if (!
|
|
162
|
+
if (!bulkDraft.reply.trim() || bulkTargetIds.length === 0)
|
|
128
163
|
return;
|
|
129
164
|
setBulkSaving(true);
|
|
130
165
|
// Partial success is the normal outcome of a bulk write, so report the
|
|
@@ -132,7 +167,7 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
132
167
|
const outcomes = await Promise.all(bulkTargetIds.map((id) => fetch(`${replyApiBase}/${id}/reply`, {
|
|
133
168
|
method: "POST",
|
|
134
169
|
headers: { "Content-Type": "application/json" },
|
|
135
|
-
body: JSON.stringify({ reply:
|
|
170
|
+
body: JSON.stringify({ reply: bulkDraft.reply }),
|
|
136
171
|
})
|
|
137
172
|
.then((res) => res.ok)
|
|
138
173
|
.catch((err) => {
|
|
@@ -145,11 +180,11 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
145
180
|
}
|
|
146
181
|
setBulkSaving(false);
|
|
147
182
|
setBulkReplyOpen(false);
|
|
148
|
-
|
|
183
|
+
setBulkDraft({ reply: "" });
|
|
149
184
|
bulkClearSelection?.();
|
|
150
185
|
setBulkTargetIds([]);
|
|
151
186
|
await refetchReviews();
|
|
152
|
-
}, [
|
|
187
|
+
}, [bulkDraft, bulkTargetIds, replyApiBase, bulkClearSelection, refetchReviews, showToast]);
|
|
153
188
|
const config = {
|
|
154
189
|
portal: "seller",
|
|
155
190
|
title: "Reviews",
|
|
@@ -193,5 +228,5 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
193
228
|
})),
|
|
194
229
|
renderCards: (rows, _view, selection, isLoading) => (_jsx(Stack, { gap: "md", children: isLoading ? (_jsx(Div, { className: "text-center", padding: "y-xl", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", children: "Loading reviews\u2026" }) })) : rows.length === 0 ? (_jsx(Div, { className: "text-center", padding: "y-3xl", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", children: "No reviews found." }) })) : (rows.map((review) => (_jsx(Div, { className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "lg", padding: "md", children: _jsxs(Row, { align: "start", justify: "between", gap: "3", wrap: true, children: [_jsxs(Row, { align: "start", className: "flex-1 min-w-0", gap: "3", children: [_jsx(Checkbox, { checked: selection.selectedIds.includes(review.id), onChange: () => selection.toggleSelect(review.id), "aria-label": "Select review" }), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { className: "truncate", weight: "medium", children: review.productTitle }), _jsxs(Row, { className: "mt-1", align: "center", gap: "sm", wrap: true, children: [_jsx(Stars, { rating: review.rating }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: ["by ", review.userName] }), review.verified && _jsx(Badge, { variant: "success", children: "Verified" }), statusBadge(review.status), _jsx(Badge, { variant: review.sellerReply ? "success" : "warning", children: review.sellerReply ? "Store replied" : "Awaiting store reply" })] }), review.title && _jsx(Text, { className: "mt-2", weight: "medium", children: review.title }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-secondary)] line-clamp-3", size: "sm", children: review.comment }), review.sellerReply && (_jsxs(Div, { className: "mt-2 pl-[0.75rem] border-l-2 border-[var(--appkit-color-primary)]", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "Store reply:" }), _jsx(Text, { size: "sm", children: review.sellerReply })] }))] })] }), _jsxs(Row, { className: "flex-shrink-0", gap: "sm", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => openReply(review), children: review.sellerReply ? "Edit Reply" : ACTIONS.STORE["reply-review"].label }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setContestTarget(review), children: ACTIONS.STORE["contest-review"].label }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setFeedbackTarget(review), children: ACTIONS.STORE["buyer-feedback"].label })] })] }) }, review.id)))) })),
|
|
195
230
|
};
|
|
196
|
-
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(SideDrawer, { isOpen: !!replyTarget, onClose: () => setReplyTarget(null), title: replyTarget?.sellerReply ? "Edit Reply" : "Reply to Review", mode: "create", children: _jsxs(Stack, { gap: "md", className: `${__P.p4}`, children: [replyTarget && (_jsxs(Div, { className: `${__P.p3} bg-[var(--appkit-color-surface-muted)]`, rounded: "default", children: [_jsx(Stars, { rating: replyTarget.rating }), _jsx(Text, { className: "mt-1", size: "sm", children: replyTarget.comment })] })), replyError && _jsx(Alert, { variant: "error", children: replyError }),
|
|
231
|
+
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(SideDrawer, { isOpen: !!replyTarget, onClose: () => setReplyTarget(null), title: replyTarget?.sellerReply ? "Edit Reply" : "Reply to Review", mode: "create", children: _jsxs(Stack, { gap: "md", className: `${__P.p4}`, children: [replyTarget && (_jsxs(Div, { className: `${__P.p3} bg-[var(--appkit-color-surface-muted)]`, rounded: "default", children: [_jsx(Stars, { rating: replyTarget.rating }), _jsx(Text, { className: "mt-1", size: "sm", children: replyTarget.comment })] })), replyError && _jsx(Alert, { variant: "error", children: replyError }), _jsx(ReviewTextForm, { schema: reviewReplySchema, values: replyDraft, onChange: (partial) => setReplyDraft((d) => ({ ...d, ...partial })), onSubmit: () => void handleReplySave(refetchReviews), submitLabel: replyTarget?.sellerReply ? "Update Reply" : "Post Reply", onCancel: () => setReplyTarget(null), isLoading: replySaving, scope: "store:review-reply" })] }) }), _jsx(Modal, { isOpen: bulkReplyOpen, onClose: () => setBulkReplyOpen(false), title: "Bulk reply to selected reviews", children: _jsx(Stack, { gap: "md", padding: "2xs", children: _jsx(ReviewTextForm, { schema: reviewBulkReplySchema, values: bulkDraft, onChange: (partial) => setBulkDraft((d) => ({ ...d, ...partial })), onSubmit: () => void submitBulkReply(), submitLabel: "Send reply", onCancel: () => setBulkReplyOpen(false), isLoading: bulkSaving, scope: "store:review-bulk-reply" }) }) }), _jsx(Modal, { isOpen: !!contestTarget, onClose: () => setContestTarget(null), title: "Contest this review", children: _jsxs(Stack, { gap: "md", padding: "2xs", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Flag this review for admin investigation." }), _jsx(ReviewTextForm, { schema: reviewContestSchema, values: contestDraft, onChange: (partial) => setContestDraft((d) => ({ ...d, ...partial })), onSubmit: () => void submitContest(refetchReviews), submitLabel: "Submit", onCancel: () => setContestTarget(null), scope: "store:review-contest" })] }) }), _jsx(Modal, { isOpen: !!feedbackTarget, onClose: () => setFeedbackTarget(null), title: "Send feedback to buyer", children: _jsx(Stack, { gap: "md", padding: "2xs", children: _jsx(ReviewTextForm, { schema: reviewFeedbackSchema, values: feedbackDraft, onChange: (partial) => setFeedbackDraft((d) => ({ ...d, ...partial })), onSubmit: () => void submitFeedback(), submitLabel: "Send", onCancel: () => setFeedbackTarget(null), scope: "store:review-feedback" }) }) })] }));
|
|
197
232
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🛑 `StoreDashboardTab`, not `StoreTab` — that name is already taken by
|
|
3
|
+
* `features/stores`, which uses it for a tab on the PUBLIC store detail page.
|
|
4
|
+
* Two same-named types in one package is the defect Root Cause #36 records, and
|
|
5
|
+
* the barrel's choice between them would be invisible at every call site.
|
|
6
|
+
*/
|
|
7
|
+
export interface StoreDashboardTab {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
/** What lives in here, for anyone who does not know the tab's name. */
|
|
11
|
+
description: string;
|
|
12
|
+
}
|
|
13
|
+
/** "Get paid": what I am owed, where it goes, on what schedule. */
|
|
14
|
+
export declare const PAYOUTS_TABS: readonly [{
|
|
15
|
+
readonly id: "payouts";
|
|
16
|
+
readonly label: "Payouts";
|
|
17
|
+
readonly description: "What you have been paid, and what is still due.";
|
|
18
|
+
}, {
|
|
19
|
+
readonly id: "methods";
|
|
20
|
+
readonly label: "Methods";
|
|
21
|
+
readonly description: "Where your money goes — bank or UPI.";
|
|
22
|
+
}, {
|
|
23
|
+
readonly id: "settings";
|
|
24
|
+
readonly label: "Settings";
|
|
25
|
+
readonly description: "How often you get paid, and the minimum.";
|
|
26
|
+
}];
|
|
27
|
+
export type PayoutsTabId = (typeof PAYOUTS_TABS)[number]["id"];
|
|
28
|
+
export declare function isPayoutsTabId(value: string | null | undefined): value is PayoutsTabId;
|
|
29
|
+
/** Shipping defaults, plus the reusable named presets built on top of them. */
|
|
30
|
+
export declare const SHIPPING_TABS: readonly [{
|
|
31
|
+
readonly id: "rates";
|
|
32
|
+
readonly label: "Rates & zones";
|
|
33
|
+
readonly description: "What you charge to ship, and where you ship to.";
|
|
34
|
+
}, {
|
|
35
|
+
readonly id: "configs";
|
|
36
|
+
readonly label: "Named configs";
|
|
37
|
+
readonly description: "Named shipping rules you can reuse per listing.";
|
|
38
|
+
}];
|
|
39
|
+
export type ShippingTabId = (typeof SHIPPING_TABS)[number]["id"];
|
|
40
|
+
export declare function isShippingTabId(value: string | null | undefined): value is ShippingTabId;
|
|
41
|
+
/** The pack-and-dispatch loop: the queue, then the paperwork it needs. */
|
|
42
|
+
export declare const FULFILLMENT_TABS: readonly [{
|
|
43
|
+
readonly id: "queue";
|
|
44
|
+
readonly label: "Queue";
|
|
45
|
+
readonly description: "Orders waiting to be packed or handed over.";
|
|
46
|
+
}, {
|
|
47
|
+
readonly id: "print";
|
|
48
|
+
readonly label: "Print centre";
|
|
49
|
+
readonly description: "Print labels, invoices and barcodes in a batch.";
|
|
50
|
+
}];
|
|
51
|
+
export type FulfillmentTabId = (typeof FULFILLMENT_TABS)[number]["id"];
|
|
52
|
+
export declare function isFulfillmentTabId(value: string | null | undefined): value is FulfillmentTabId;
|
|
53
|
+
/** The shop's identity — its look, and the address buyers reach it at. */
|
|
54
|
+
export declare const STOREFRONT_TABS: readonly [{
|
|
55
|
+
readonly id: "profile";
|
|
56
|
+
readonly label: "Profile";
|
|
57
|
+
readonly description: "Your shop's name, logo, banner and description.";
|
|
58
|
+
}, {
|
|
59
|
+
readonly id: "slug";
|
|
60
|
+
readonly label: "Web address";
|
|
61
|
+
readonly description: "The web address buyers reach your shop at.";
|
|
62
|
+
}];
|
|
63
|
+
export type StorefrontTabId = (typeof STOREFRONT_TABS)[number]["id"];
|
|
64
|
+
export declare function isStorefrontTabId(value: string | null | undefined): value is StorefrontTabId;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: W8's C2 folds sibling store pages that are one task into one tabbed
|
|
3
|
+
* page. Each absorbed page keeps a `?tab=` deep link so it stays findable
|
|
4
|
+
* from the command palette and from a bookmark, and the action index's
|
|
5
|
+
* `AIX_DEAD_DEEPLINK` check needs a union to validate those links
|
|
6
|
+
* against — a deep link naming a tab that does not exist opens the page
|
|
7
|
+
* on its default tab and reads as the search having missed.
|
|
8
|
+
*
|
|
9
|
+
* ## 🛑 Tab ids are NOT nav hrefs
|
|
10
|
+
*
|
|
11
|
+
* `navItemId(portal, href)` strips the query, so `/store/payouts` and
|
|
12
|
+
* `/store/payouts?tab=methods` produce the SAME nav id — which is at once the
|
|
13
|
+
* `navConfig` toggle key, the gate that makes `requiredPermission` run, and the
|
|
14
|
+
* action-index entry id. So the absorbed pages keep their own hrefs (as
|
|
15
|
+
* redirect shells) and these ids live only in `?tab=`.
|
|
16
|
+
*
|
|
17
|
+
* EXPORTS:
|
|
18
|
+
* PAYOUTS_TABS / isPayoutsTabId, SHIPPING_TABS / isShippingTabId,
|
|
19
|
+
* FULFILLMENT_TABS / isFulfillmentTabId, STOREFRONT_TABS / isStorefrontTabId
|
|
20
|
+
*
|
|
21
|
+
* @tag domain:seller
|
|
22
|
+
* @tag layer:constants
|
|
23
|
+
* @tag pattern:registry
|
|
24
|
+
* @tag access:isomorphic
|
|
25
|
+
* @tag consumers:store payouts/shipping/fulfillment/storefront pages,action index
|
|
26
|
+
* @tag sideEffects:none
|
|
27
|
+
*/
|
|
28
|
+
/** "Get paid": what I am owed, where it goes, on what schedule. */
|
|
29
|
+
export const PAYOUTS_TABS = [
|
|
30
|
+
{ id: "payouts", label: "Payouts", description: "What you have been paid, and what is still due." },
|
|
31
|
+
{ id: "methods", label: "Methods", description: "Where your money goes — bank or UPI." },
|
|
32
|
+
{ id: "settings", label: "Settings", description: "How often you get paid, and the minimum." },
|
|
33
|
+
];
|
|
34
|
+
export function isPayoutsTabId(value) {
|
|
35
|
+
return !!value && PAYOUTS_TABS.some((t) => t.id === value);
|
|
36
|
+
}
|
|
37
|
+
/** Shipping defaults, plus the reusable named presets built on top of them. */
|
|
38
|
+
export const SHIPPING_TABS = [
|
|
39
|
+
{ id: "rates", label: "Rates & zones", description: "What you charge to ship, and where you ship to." },
|
|
40
|
+
{ id: "configs", label: "Named configs", description: "Named shipping rules you can reuse per listing." },
|
|
41
|
+
];
|
|
42
|
+
export function isShippingTabId(value) {
|
|
43
|
+
return !!value && SHIPPING_TABS.some((t) => t.id === value);
|
|
44
|
+
}
|
|
45
|
+
/** The pack-and-dispatch loop: the queue, then the paperwork it needs. */
|
|
46
|
+
export const FULFILLMENT_TABS = [
|
|
47
|
+
{ id: "queue", label: "Queue", description: "Orders waiting to be packed or handed over." },
|
|
48
|
+
{ id: "print", label: "Print centre", description: "Print labels, invoices and barcodes in a batch." },
|
|
49
|
+
];
|
|
50
|
+
export function isFulfillmentTabId(value) {
|
|
51
|
+
return !!value && FULFILLMENT_TABS.some((t) => t.id === value);
|
|
52
|
+
}
|
|
53
|
+
/** The shop's identity — its look, and the address buyers reach it at. */
|
|
54
|
+
export const STOREFRONT_TABS = [
|
|
55
|
+
{ id: "profile", label: "Profile", description: "Your shop's name, logo, banner and description." },
|
|
56
|
+
{ id: "slug", label: "Web address", description: "The web address buyers reach your shop at." },
|
|
57
|
+
];
|
|
58
|
+
export function isStorefrontTabId(value) {
|
|
59
|
+
return !!value && STOREFRONT_TABS.some((t) => t.id === value);
|
|
60
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* 🛑 `""` is a real, meaningful value here — it is "keep the current status",
|
|
4
|
+
* which is what the panel's first option means and what its diff-against-the-
|
|
5
|
+
* loaded-order payload relies on. It is not an empty required field.
|
|
6
|
+
*/
|
|
7
|
+
export declare const sellerOrderUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
8
|
+
status: z.ZodEnum<["", "confirmed", "processing", "shipped", "delivered", "cancelled"]>;
|
|
9
|
+
trackingNumber: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
10
|
+
carrier: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
11
|
+
trackingUrl: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
status: "" | "cancelled" | "processing" | "delivered" | "confirmed" | "shipped";
|
|
14
|
+
trackingNumber?: string | undefined;
|
|
15
|
+
trackingUrl?: string | undefined;
|
|
16
|
+
carrier?: string | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
status: "" | "cancelled" | "processing" | "delivered" | "confirmed" | "shipped";
|
|
19
|
+
trackingNumber?: string | undefined;
|
|
20
|
+
trackingUrl?: string | undefined;
|
|
21
|
+
carrier?: string | undefined;
|
|
22
|
+
}>, {
|
|
23
|
+
status: "" | "cancelled" | "processing" | "delivered" | "confirmed" | "shipped";
|
|
24
|
+
trackingNumber?: string | undefined;
|
|
25
|
+
trackingUrl?: string | undefined;
|
|
26
|
+
carrier?: string | undefined;
|
|
27
|
+
}, {
|
|
28
|
+
status: "" | "cancelled" | "processing" | "delivered" | "confirmed" | "shipped";
|
|
29
|
+
trackingNumber?: string | undefined;
|
|
30
|
+
trackingUrl?: string | undefined;
|
|
31
|
+
carrier?: string | undefined;
|
|
32
|
+
}>;
|
|
33
|
+
export type SellerOrderUpdateValues = z.infer<typeof sellerOrderUpdateSchema>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: The seller's "Update order" panel wrote a status change, a tracking
|
|
3
|
+
* number, a carrier and a tracking URL with no schema at all — the one
|
|
4
|
+
* form in the seller dashboard that changes what a BUYER sees about a
|
|
5
|
+
* shipment they are waiting on.
|
|
6
|
+
*
|
|
7
|
+
* Two rules were missing, both invisible until a buyer complained. A
|
|
8
|
+
* tracking URL was a free string, so a typo'd or non-http value went
|
|
9
|
+
* straight into the tracking link a buyer clicks. And a status could move
|
|
10
|
+
* to `shipped` with no tracking number at all — the seller's own carrier
|
|
11
|
+
* fields sat right there, empty, and nothing said so.
|
|
12
|
+
*
|
|
13
|
+
* WHAT: The four fields the panel offers, annotated for
|
|
14
|
+
* `buildSectionsFromSchema`.
|
|
15
|
+
*
|
|
16
|
+
* EXPORTS:
|
|
17
|
+
* sellerOrderUpdateSchema, type SellerOrderUpdateValues
|
|
18
|
+
*
|
|
19
|
+
* @tag domain:seller,orders
|
|
20
|
+
* @tag layer:schema
|
|
21
|
+
* @tag pattern:none
|
|
22
|
+
* @tag access:isomorphic
|
|
23
|
+
* @tag consumers:SellerOrderDetailPanel
|
|
24
|
+
* @tag sideEffects:none
|
|
25
|
+
*/
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
28
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
29
|
+
/**
|
|
30
|
+
* 🛑 `""` is a real, meaningful value here — it is "keep the current status",
|
|
31
|
+
* which is what the panel's first option means and what its diff-against-the-
|
|
32
|
+
* loaded-order payload relies on. It is not an empty required field.
|
|
33
|
+
*/
|
|
34
|
+
export const sellerOrderUpdateSchema = z
|
|
35
|
+
.object({
|
|
36
|
+
status: annotate(z.enum(["", "confirmed", "processing", "shipped", "delivered", "cancelled"]), {
|
|
37
|
+
section: "status", sectionLabel: "Update order", sectionRequired: true,
|
|
38
|
+
quick: true, order: 1, row: "full", kind: "select", label: "New status",
|
|
39
|
+
}),
|
|
40
|
+
trackingNumber: annotate(z.string().trim().max(120).optional().or(z.literal("")), {
|
|
41
|
+
section: "shipping", sectionLabel: "Shipment", order: 1, row: "pair",
|
|
42
|
+
label: "Tracking number",
|
|
43
|
+
}),
|
|
44
|
+
carrier: annotate(z.string().trim().max(80).optional().or(z.literal("")), {
|
|
45
|
+
section: "shipping", order: 2, row: "pair",
|
|
46
|
+
}),
|
|
47
|
+
trackingUrl: annotate(z.string().trim().max(2000).optional().or(z.literal("")), {
|
|
48
|
+
section: "shipping", order: 3, row: "full", inputType: "url",
|
|
49
|
+
label: "Tracking URL (optional)",
|
|
50
|
+
}),
|
|
51
|
+
})
|
|
52
|
+
.superRefine((v, ctx) => {
|
|
53
|
+
const issue = (path, message) => ctx.addIssue({ code: z.ZodIssueCode.custom, path: [path], message });
|
|
54
|
+
// Marking an order shipped is what starts the buyer's "where is it"
|
|
55
|
+
// question, and the field that answers it is on this same panel.
|
|
56
|
+
if (v.status === "shipped" && !v.trackingNumber?.trim()) {
|
|
57
|
+
issue("trackingNumber", "A shipped order needs a tracking number.");
|
|
58
|
+
}
|
|
59
|
+
const url = v.trackingUrl?.trim();
|
|
60
|
+
if (url) {
|
|
61
|
+
let parsed = null;
|
|
62
|
+
try {
|
|
63
|
+
parsed = new URL(url);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
void normalizeError(err);
|
|
67
|
+
parsed = null;
|
|
68
|
+
}
|
|
69
|
+
if (!parsed)
|
|
70
|
+
issue("trackingUrl", "That is not a valid URL.");
|
|
71
|
+
else if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
72
|
+
issue("trackingUrl", "A tracking URL must start with http or https.");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Matches `replySchema` on `POST /api/store/reviews/[id]/reply`. */
|
|
3
|
+
export declare const reviewReplySchema: z.ZodObject<{
|
|
4
|
+
reply: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
reply: string;
|
|
7
|
+
}, {
|
|
8
|
+
reply: string;
|
|
9
|
+
}>;
|
|
10
|
+
export type ReviewReplyValues = z.infer<typeof reviewReplySchema>;
|
|
11
|
+
/** The same reply, posted on every selected review. */
|
|
12
|
+
export declare const reviewBulkReplySchema: z.ZodObject<{
|
|
13
|
+
reply: z.ZodString;
|
|
14
|
+
}, "strip", z.ZodTypeAny, {
|
|
15
|
+
reply: string;
|
|
16
|
+
}, {
|
|
17
|
+
reply: string;
|
|
18
|
+
}>;
|
|
19
|
+
export type ReviewBulkReplyValues = z.infer<typeof reviewBulkReplySchema>;
|
|
20
|
+
/** Flags a review for admin investigation. */
|
|
21
|
+
export declare const reviewContestSchema: z.ZodObject<{
|
|
22
|
+
reason: z.ZodString;
|
|
23
|
+
}, "strip", z.ZodTypeAny, {
|
|
24
|
+
reason: string;
|
|
25
|
+
}, {
|
|
26
|
+
reason: string;
|
|
27
|
+
}>;
|
|
28
|
+
export type ReviewContestValues = z.infer<typeof reviewContestSchema>;
|
|
29
|
+
/** A private note to the buyer, never shown on the public review. */
|
|
30
|
+
export declare const reviewFeedbackSchema: z.ZodObject<{
|
|
31
|
+
feedback: z.ZodString;
|
|
32
|
+
}, "strip", z.ZodTypeAny, {
|
|
33
|
+
feedback: string;
|
|
34
|
+
}, {
|
|
35
|
+
feedback: string;
|
|
36
|
+
}>;
|
|
37
|
+
export type ReviewFeedbackValues = z.infer<typeof reviewFeedbackSchema>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: The seller's reviews page has FOUR write surfaces — reply, bulk reply,
|
|
3
|
+
* contest, send-feedback — and not one of them had a schema. Every guard
|
|
4
|
+
* was a `disabled={!text.trim()}` on the button, which is a UI hint, not a
|
|
5
|
+
* rule: it says nothing about the ceiling the route enforces, and it
|
|
6
|
+
* cannot say anything at all about content.
|
|
7
|
+
*
|
|
8
|
+
* The reply route caps at 1000 characters. The textarea carried
|
|
9
|
+
* `maxLength={1000}`, so the browser truncated silently rather than
|
|
10
|
+
* saying so — a seller writing a long reply lost the tail with no message.
|
|
11
|
+
* A schema puts the same number where it can be reported.
|
|
12
|
+
*
|
|
13
|
+
* The contest route trims and requires a non-empty `reason` and nothing
|
|
14
|
+
* more, so "x" filed a moderation request a human then has to interpret.
|
|
15
|
+
* A ten-character floor matches the admin offer-cancel reason, which is
|
|
16
|
+
* the same kind of record: the only durable account of why.
|
|
17
|
+
*
|
|
18
|
+
* WHAT: One schema per surface. Each is a single field, and that is the point —
|
|
19
|
+
* they are the same shape as `hardBanReasonSchema`, and they get the same
|
|
20
|
+
* error summary and section badge as every other form.
|
|
21
|
+
*
|
|
22
|
+
* EXPORTS:
|
|
23
|
+
* reviewReplySchema, reviewBulkReplySchema, reviewContestSchema,
|
|
24
|
+
* reviewFeedbackSchema, and their value types
|
|
25
|
+
*
|
|
26
|
+
* @tag domain:seller,reviews
|
|
27
|
+
* @tag layer:schema
|
|
28
|
+
* @tag pattern:none
|
|
29
|
+
* @tag access:isomorphic
|
|
30
|
+
* @tag consumers:SellerReviewsView
|
|
31
|
+
* @tag sideEffects:none
|
|
32
|
+
*/
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
35
|
+
/** Matches `replySchema` on `POST /api/store/reviews/[id]/reply`. */
|
|
36
|
+
export const reviewReplySchema = z.object({
|
|
37
|
+
reply: annotate(z
|
|
38
|
+
.string()
|
|
39
|
+
.trim()
|
|
40
|
+
.min(1, "Write a reply before posting it.")
|
|
41
|
+
.max(1000, "A reply is capped at 1000 characters."), {
|
|
42
|
+
section: "reply", sectionLabel: "Store reply", sectionRequired: true,
|
|
43
|
+
quick: true, order: 1, row: "full", kind: "textarea",
|
|
44
|
+
help: "Shown publicly beneath the buyer's review.",
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
/** The same reply, posted on every selected review. */
|
|
48
|
+
export const reviewBulkReplySchema = z.object({
|
|
49
|
+
reply: annotate(z
|
|
50
|
+
.string()
|
|
51
|
+
.trim()
|
|
52
|
+
.min(1, "Write a reply before sending it.")
|
|
53
|
+
.max(1000, "A reply is capped at 1000 characters."), {
|
|
54
|
+
section: "reply", sectionLabel: "Reply", sectionRequired: true,
|
|
55
|
+
quick: true, order: 1, row: "full", kind: "textarea",
|
|
56
|
+
help: "The same reply will be posted on all selected reviews.",
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
/** Flags a review for admin investigation. */
|
|
60
|
+
export const reviewContestSchema = z.object({
|
|
61
|
+
reason: annotate(z
|
|
62
|
+
.string()
|
|
63
|
+
.trim()
|
|
64
|
+
.min(10, "Say why this review should be investigated — at least 10 characters.")
|
|
65
|
+
.max(1000), {
|
|
66
|
+
section: "contest", sectionLabel: "Reason", sectionRequired: true,
|
|
67
|
+
quick: true, order: 1, row: "full", kind: "textarea",
|
|
68
|
+
help: "Fake, abusive, off-topic — be specific; a moderator reads this.",
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
/** A private note to the buyer, never shown on the public review. */
|
|
72
|
+
export const reviewFeedbackSchema = z.object({
|
|
73
|
+
feedback: annotate(z
|
|
74
|
+
.string()
|
|
75
|
+
.trim()
|
|
76
|
+
.min(1, "Write your message before sending it.")
|
|
77
|
+
.max(1000, "Keep the message under 1000 characters."), {
|
|
78
|
+
section: "feedback", sectionLabel: "Feedback", sectionRequired: true,
|
|
79
|
+
quick: true, order: 1, row: "full", kind: "textarea",
|
|
80
|
+
help: "Sent to the buyer's notification inbox. Does not appear on the review.",
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
@@ -11,6 +11,6 @@ export { deriveQuickFields, deriveQuickFieldNames, allDefinitionFields, applyDer
|
|
|
11
11
|
export type { FieldUiMeta, FieldRow, FieldKind, CalculatedTier, } from "./field-ui-meta";
|
|
12
12
|
export { ui, annotate, ROW_COLUMNS, inferFieldUiMeta, resolveFieldUiMeta, unwrapSchema, schemaEnumOptions, schemaIsOptional, objectShape, } from "./field-ui-meta";
|
|
13
13
|
export type { ResolvedField, FieldRenderer, BuildSectionsOptions, } from "./build-sections";
|
|
14
|
-
export { buildSectionsFromSchema, resolveFields, groupFieldsBySection, packRows, } from "./build-sections";
|
|
14
|
+
export { buildSectionsFromSchema, resolveFields, groupFieldsBySection, packRows, visibleValues, } from "./build-sections";
|
|
15
15
|
export type { TitleDescriptionGroupProps, ImageFieldGroupProps, SeoFieldGroupProps, StatusFieldGroupProps, StatusFieldGroupOption, } from "./field-groups";
|
|
16
16
|
export { TitleDescriptionGroup, ImageFieldGroup, SeoFieldGroup, StatusFieldGroup, } from "./field-groups";
|
|
@@ -4,5 +4,9 @@ export { QuickFormDrawer } from "./QuickFormDrawer";
|
|
|
4
4
|
export { SectionForm, useSectionFormNav, orderSections, buildFieldToSectionIndex, sectionAnchorId, } from "./SectionForm";
|
|
5
5
|
export { deriveQuickFields, deriveQuickFieldNames, allDefinitionFields, applyDerive, assertNoDeriveCollision, humaniseFieldName, } from "./entity-form";
|
|
6
6
|
export { ui, annotate, ROW_COLUMNS, inferFieldUiMeta, resolveFieldUiMeta, unwrapSchema, schemaEnumOptions, schemaIsOptional, objectShape, } from "./field-ui-meta";
|
|
7
|
-
export { buildSectionsFromSchema, resolveFields, groupFieldsBySection, packRows,
|
|
7
|
+
export { buildSectionsFromSchema, resolveFields, groupFieldsBySection, packRows,
|
|
8
|
+
// The payload builder. A `when` predicate hides a CONTROL, not a VALUE, so a
|
|
9
|
+
// draft can still carry a field the user can no longer see — build the
|
|
10
|
+
// submitted object from this, never from the raw draft.
|
|
11
|
+
visibleValues, } from "./build-sections";
|
|
8
12
|
export { TitleDescriptionGroup, ImageFieldGroup, SeoFieldGroup, StatusFieldGroup, } from "./field-groups";
|