@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.
Files changed (71) hide show
  1. package/dist/_internal/server/features/products/list-public.d.ts +18 -0
  2. package/dist/_internal/server/features/products/list-public.js +27 -9
  3. package/dist/client.d.ts +9 -0
  4. package/dist/client.js +12 -0
  5. package/dist/features/account/components/UserBidsView.js +6 -1
  6. package/dist/features/account/components/UserOrdersView.js +8 -1
  7. package/dist/features/account/components/UserReturnsView.js +6 -1
  8. package/dist/features/admin/components/AdminAdsView.js +36 -34
  9. package/dist/features/admin/components/AdminAuditLogView.js +12 -1
  10. package/dist/features/admin/components/AdminBundlesView.js +5 -1
  11. package/dist/features/admin/components/AdminCarouselEditorView.js +78 -10
  12. package/dist/features/admin/components/AdminFaqsView.js +10 -1
  13. package/dist/features/admin/components/AdminNotificationsView.js +7 -1
  14. package/dist/features/admin/components/AdminOrderEditorView.js +55 -24
  15. package/dist/features/admin/components/AdminPrizeDrawsView.js +11 -1
  16. package/dist/features/admin/components/AdminProductEditorView.js +78 -18
  17. package/dist/features/admin/components/AdminSectionsView.js +89 -32
  18. package/dist/features/admin/components/AdminSiteSettingsView.js +96 -44
  19. package/dist/features/admin/components/AdminStoresView.js +13 -1
  20. package/dist/features/admin/components/AdminSupportTicketDetailView.js +58 -43
  21. package/dist/features/admin/components/AdminSupportTicketsView.js +11 -1
  22. package/dist/features/admin/components/AdminTeamView.js +11 -1
  23. package/dist/features/admin/components/AdminTesterChecklistView.js +6 -1
  24. package/dist/features/admin/components/AdminUserEditorView.js +65 -82
  25. package/dist/features/admin/components/analytics/AdminPageViewsReportView.js +6 -1
  26. package/dist/features/admin/constants/admin-page-tabs.d.ts +36 -0
  27. package/dist/features/admin/constants/admin-page-tabs.js +45 -0
  28. package/dist/features/admin/schemas/admin-ops-forms.d.ts +81 -0
  29. package/dist/features/admin/schemas/admin-ops-forms.js +123 -1
  30. package/dist/features/admin/schemas/admin-user-form.d.ts +78 -3
  31. package/dist/features/admin/schemas/admin-user-form.js +96 -7
  32. package/dist/features/admin/schemas/homepage-section-form.d.ts +9 -0
  33. package/dist/features/layout/PageTabs.d.ts +17 -0
  34. package/dist/features/layout/PageTabs.js +45 -0
  35. package/dist/features/products/components/GroupSettingsPanel.js +62 -16
  36. package/dist/features/search/action-index/tab-entries.d.ts +18 -0
  37. package/dist/features/search/action-index/tab-entries.js +128 -0
  38. package/dist/features/seller/components/SellerBundlesView.js +5 -1
  39. package/dist/features/seller/components/SellerGoogleReviewsView.js +31 -19
  40. package/dist/features/seller/components/SellerOrdersView.js +57 -19
  41. package/dist/features/seller/components/SellerProductsView.js +10 -1
  42. package/dist/features/seller/components/SellerReviewsView.d.ts +2 -1
  43. package/dist/features/seller/components/SellerReviewsView.js +54 -19
  44. package/dist/features/seller/constants/store-tabs.d.ts +64 -0
  45. package/dist/features/seller/constants/store-tabs.js +60 -0
  46. package/dist/features/seller/schemas/order-forms.d.ts +33 -0
  47. package/dist/features/seller/schemas/order-forms.js +75 -0
  48. package/dist/features/seller/schemas/review-forms.d.ts +37 -0
  49. package/dist/features/seller/schemas/review-forms.js +82 -0
  50. package/dist/features/shell/index.d.ts +1 -1
  51. package/dist/features/shell/index.js +5 -1
  52. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +44 -15
  53. package/dist/features/shipments/schemas/validation.d.ts +41 -0
  54. package/dist/features/shipments/schemas/validation.js +49 -0
  55. package/dist/features/store-extensions/schemas/google-config-form.d.ts +36 -0
  56. package/dist/features/store-extensions/schemas/google-config-form.js +24 -0
  57. package/dist/features/stores/actions/store-query-actions.js +5 -3
  58. package/dist/features/stores/repository/store.repository.d.ts +3 -1
  59. package/dist/features/stores/repository/store.repository.js +6 -2
  60. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +79 -0
  61. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +53 -22
  62. package/dist/index.d.ts +4 -0
  63. package/dist/index.js +2 -0
  64. package/dist/next/index.d.ts +2 -0
  65. package/dist/next/index.js +1 -0
  66. package/dist/react/hooks/useTabParam.d.ts +19 -0
  67. package/dist/react/hooks/useTabParam.js +65 -0
  68. package/dist/schemas/registry.d.ts +27 -3
  69. package/dist/styles.css +1 -1
  70. package/dist/tailwind-utilities.css +1 -1
  71. 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, Textarea, useToast } from "../../../ui";
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 [replyText, setReplyText] = useState("");
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 [bulkReplyText, setBulkReplyText] = useState("");
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 [contestReason, setContestReason] = useState("");
86
+ const [contestDraft, setContestDraft] = useState({ reason: "" });
52
87
  const [feedbackTarget, setFeedbackTarget] = useState(null);
53
- const [feedbackText, setFeedbackText] = useState("");
88
+ const [feedbackDraft, setFeedbackDraft] = useState({ feedback: "" });
54
89
  const openReply = (review) => {
55
90
  setReplyTarget(review);
56
- setReplyText(review.sellerReply ?? "");
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: replyText }),
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: contestReason }),
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
- setContestReason("");
139
+ setContestDraft({ reason: "" });
105
140
  onDone();
106
- }, [contestTarget, contestReason, showToast]);
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: feedbackText }),
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
- setFeedbackText("");
125
- }, [feedbackTarget, feedbackText, showToast]);
159
+ setFeedbackDraft({ feedback: "" });
160
+ }, [feedbackTarget, feedbackDraft, showToast]);
126
161
  const submitBulkReply = useCallback(async () => {
127
- if (!bulkReplyText.trim() || bulkTargetIds.length === 0)
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: bulkReplyText }),
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
- setBulkReplyText("");
183
+ setBulkDraft({ reply: "" });
149
184
  bulkClearSelection?.();
150
185
  setBulkTargetIds([]);
151
186
  await refetchReviews();
152
- }, [bulkReplyText, bulkTargetIds, replyApiBase, bulkClearSelection, refetchReviews, showToast]);
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 }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-1.5", size: "sm", weight: "medium", children: "Store reply" }), _jsx(Textarea, { rows: 5, value: replyText, onChange: (e) => setReplyText(e.target.value), maxLength: 1000, placeholder: "Write your response to this review\u2026" }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)] mt-1", size: "xs", align: "end", children: [replyText.length, "/1000"] })] }), _jsxs(Row, { justify: "end", gap: "sm", children: [_jsx(Button, { variant: "outline", onClick: () => setReplyTarget(null), disabled: replySaving, children: "Cancel" }), _jsx(Button, { variant: "primary", onClick: () => handleReplySave(refetchReviews), disabled: replySaving || !replyText.trim(), isLoading: replySaving, children: replyTarget?.sellerReply ? "Update Reply" : "Post Reply" })] })] }) }), _jsx(Modal, { isOpen: bulkReplyOpen, onClose: () => setBulkReplyOpen(false), title: "Bulk reply to selected reviews", actions: _jsxs(Row, { gap: "sm", children: [_jsx(Button, { variant: "ghost", onClick: () => setBulkReplyOpen(false), disabled: bulkSaving, children: "Cancel" }), _jsx(Button, { variant: "primary", onClick: () => void submitBulkReply(), disabled: !bulkReplyText.trim() || bulkSaving, isLoading: bulkSaving, children: "Send reply" })] }), children: _jsxs(Stack, { gap: "md", padding: "2xs", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "The same reply will be posted on all selected reviews." }), _jsx(Textarea, { value: bulkReplyText, onChange: (e) => setBulkReplyText(e.target.value), rows: 5, placeholder: "Thanks for your review\u2026", label: "Reply" })] }) }), _jsx(Modal, { isOpen: !!contestTarget, onClose: () => setContestTarget(null), title: "Contest this review", actions: _jsxs(Row, { gap: "sm", children: [_jsx(Button, { variant: "ghost", onClick: () => setContestTarget(null), children: "Cancel" }), _jsx(Button, { variant: "primary", onClick: () => void submitContest(refetchReviews), disabled: !contestReason.trim(), children: "Submit" })] }), 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. Provide a clear reason \u2014 fake, abusive, off-topic, etc." }), _jsx(Textarea, { value: contestReason, onChange: (e) => setContestReason(e.target.value), rows: 4, label: "Reason" })] }) }), _jsx(Modal, { isOpen: !!feedbackTarget, onClose: () => setFeedbackTarget(null), title: "Send feedback to buyer", actions: _jsxs(Row, { gap: "sm", children: [_jsx(Button, { variant: "ghost", onClick: () => setFeedbackTarget(null), children: "Cancel" }), _jsx(Button, { variant: "primary", onClick: () => void submitFeedback(), disabled: !feedbackText.trim(), children: "Send" })] }), children: _jsxs(Stack, { gap: "md", padding: "2xs", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Private message sent to the buyer's notification inbox. Does not appear on the public review." }), _jsx(Textarea, { value: feedbackText, onChange: (e) => setFeedbackText(e.target.value), rows: 4, label: "Feedback" })] }) })] }));
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, } from "./build-sections";
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";