@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,9 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { normalizeError } from "../../../errors/normalize";
4
- import { useEffect, useState } from "react";
4
+ import React, { useEffect, useState } from "react";
5
5
  import { z } from "zod";
6
- import { Button, Code, ConfirmDeleteModal, Div, FormField, Heading, Modal, Row, Select, SideDrawer, Span, Stack, Tabs, TabsContent, TabsList, TabsTrigger, Text, useToast } from "../../../ui";
6
+ import { Button, Code, ConfirmDeleteModal, Div, Heading, Modal, Row, SideDrawer, Span, Stack, Tabs, TabsContent, TabsList, TabsTrigger, Text, useToast } from "../../../ui";
7
7
  import { FormShellContext, useFormShellState, FormErrorSummary } from "../../../ui/forms";
8
8
  import { MediaImage } from "../../media/MediaImage";
9
9
  import { apiClient } from "../../../http";
@@ -11,13 +11,33 @@ import { PRODUCT_ENDPOINTS } from "../../../constants/api-endpoints";
11
11
  import { ProductInlineSelect } from "../../seller/components/ProductInlineSelect";
12
12
  import { SIEVE_OP, sieveFilter } from "../../../utils/sieve-builder";
13
13
  import { PRODUCT_FIELDS } from "../../../constants/field-names";
14
+ import { annotate } from "../../shell/field-ui-meta";
15
+ import { SectionForm, useSectionFormNav, buildSectionsFromSchema, visibleValues, } from "../../shell";
16
+ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
14
17
  const groupTitleSchema = z.object({
15
- groupTitle: z.string().min(1, "Group title is required"),
18
+ groupTitle: annotate(z.string().min(1, "Group title is required"), {
19
+ section: "group", sectionLabel: "Group", sectionRequired: true, quick: true,
20
+ order: 1, row: "full", label: "Group title",
21
+ help: "Shown above the set on the parent listing.",
22
+ }),
16
23
  });
24
+ /*
25
+ * `price` stays a STRING rather than `z.coerce.number()`, deliberately: the
26
+ * POST body sends `parseFloat(price)` and an empty numeric input coerces to
27
+ * `NaN`, which passes `.optional()` and lands in the payload as `null`. As a
28
+ * string the "enter a valid price" rule can be stated once and reported on the
29
+ * field.
30
+ */
17
31
  const createChildSchema = z.object({
18
- title: z.string().min(1, "Title is required"),
19
- price: z.string().refine((v) => v.trim() !== "" && !Number.isNaN(parseFloat(v)) && parseFloat(v) > 0, "Enter a valid price"),
20
- condition: z.string(),
32
+ title: annotate(z.string().min(1, "Title is required"), {
33
+ section: "child", sectionLabel: "New child listing", sectionRequired: true,
34
+ quick: true, order: 1, row: "pair", label: "Title",
35
+ }),
36
+ price: annotate(z.string().refine((v) => v.trim() !== "" && !Number.isNaN(parseFloat(v)) && parseFloat(v) > 0, "Enter a valid price"), { section: "child", quick: true, order: 2, row: "pair", kind: "number", label: "Price (INR)" }),
37
+ condition: annotate(z.string(), {
38
+ section: "child", order: 3, row: "pair", kind: "select", label: "Condition",
39
+ help: "Category, brand, shipping and return policy are inherited from the parent listing.",
40
+ }),
21
41
  });
22
42
  const __P = {
23
43
  p4: "p-[var(--appkit-space-4)]",
@@ -34,7 +54,7 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
34
54
  const { showToast } = useToast();
35
55
  const [open, setOpen] = useState(false);
36
56
  const [loading, setLoading] = useState(false);
37
- const [editTitle, setEditTitle] = useState(groupTitle ?? "");
57
+ const [titleDraft, setTitleDraft] = useState({ groupTitle: groupTitle ?? "" });
38
58
  const [showAddModal, setShowAddModal] = useState(false);
39
59
  const [addTab, setAddTab] = useState("create");
40
60
  const [createForm, setCreateForm] = useState({
@@ -45,11 +65,18 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
45
65
  const [linkTargets, setLinkTargets] = useState([]);
46
66
  const [children, setChildren] = useState(null);
47
67
  const [confirmAction, setConfirmAction] = useState(null);
48
- const { shellCtx: titleShellCtx, validate: validateTitle } = useFormShellState(groupTitleSchema);
68
+ const titleSections = React.useMemo(() => buildSectionsFromSchema(groupTitleSchema), []);
69
+ const titleNav = useSectionFormNav(titleSections, titleDraft, { scope: "store:group-title" });
70
+ const titleForm = useFormShellState(groupTitleSchema, {
71
+ sections: titleNav.sectionMeta,
72
+ onGoToSection: titleNav.goToSection,
73
+ fieldToSectionIndex: titleNav.fieldToSectionIndex,
74
+ });
75
+ const titleShellCtx = titleForm.shellCtx;
49
76
  useEffect(() => {
50
- validateTitle({ groupTitle: editTitle });
77
+ titleForm.validate(titleDraft);
51
78
  // eslint-disable-next-line react-hooks/exhaustive-deps
52
- }, [editTitle, validateTitle]);
79
+ }, [titleDraft]);
53
80
  if (isAuction)
54
81
  return null;
55
82
  const groupEndpoint = `${storeProductsEndpoint}/${productId}/group`;
@@ -90,7 +117,7 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
90
117
  return;
91
118
  setLoading(true);
92
119
  try {
93
- await apiClient.patch(groupEndpoint, { groupTitle: editTitle });
120
+ await apiClient.patch(groupEndpoint, visibleValues(groupTitleSchema, titleDraft));
94
121
  showToast("Group title saved.", "success");
95
122
  onGroupChanged();
96
123
  }
@@ -237,17 +264,36 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
237
264
  setOpen(next);
238
265
  if (next && isGroupParent && children === null)
239
266
  loadChildren();
240
- }, paddingX: "none", paddingY: "xs", justify: "between", className: "w-full text-left group", "aria-expanded": open, children: [_jsx(Heading, { level: 3, size: "sm", weight: "semibold", color: "muted", children: "Group Settings" }), _jsx(Span, { size: "xs", className: "group-hover:text-[var(--appkit-color-text-muted)] dark:group-hover:text-[var(--appkit-color-text-faint)] transition-colors", color: "faint", children: open ? "▲" : "▼" })] }), open && (_jsxs(Stack, { gap: "sm", className: `mt-3 ${__P.p4}`, surface: "muted", rounded: "lg", border: "default", children: [!groupId && !groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", children: "Group related listings together \u2014 e.g. a set, bundle, or multi-part item. Parts can be sold individually but shown together." }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: startGroup, isLoading: loading, children: "Start a group" })] })), isGroupParent && groupId && (_jsxs(Stack, { gap: "md", children: [_jsxs(FormShellContext.Provider, { value: titleShellCtx, children: [_jsxs(Row, { align: "start", gap: "sm", wrap: true, children: [_jsx(Div, { className: "flex-1 min-w-[200px]", children: _jsx(FormField, { name: "groupTitle", label: "Group title", type: "text", value: editTitle, onChange: setEditTitle, placeholder: "e.g. Human Toy Complete Set" }) }), _jsx(Div, { padding: "t-lg", children: _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: saveTitle, isLoading: loading, children: "Save title" }) })] }), _jsx(FormErrorSummary, {})] }), _jsxs(Div, { children: [_jsxs(Text, { className: "mb-2", color: "muted", size: "xs", weight: "semibold", children: ["Members (", childSlugsCount + 1, " \u2014 including this listing)"] }), loading && !children ? (_jsx(Text, { size: "xs", color: "faint", children: "Loading\u2026" })) : (_jsxs(Div, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", children: [_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [_jsx(Span, { padding: "pill-2xs", weight: "semibold", className: "bg-[var(--appkit-color-primary)]/10 text-[var(--appkit-color-primary)] text-[10px]", rounded: "default", children: "Parent" }), _jsx(Text, { className: "flex-1", color: "primary", size: "sm", children: productSlug })] }), (children ?? []).filter((c) => c.id !== productId).map((child) => (_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [child.images?.[0] ? (_jsx(Div, { className: "relative w-8 h-8 flex-shrink-0 border border-[var(--appkit-color-border)]", rounded: "full", overflow: "hidden", children: _jsx(MediaImage, { src: child.images[0], alt: child.title, size: "thumbnail" }) })) : (_jsx(Div, { className: "w-8 h-8", surface: "subtle", rounded: "full" })), _jsx(Text, { className: "flex-1 truncate", color: "primary", size: "sm", children: child.title }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => requestUnlinkChild(child.id), isLoading: loading, className: `${CLS_DELETE_LINK} text-[length:var(--appkit-text-xs)]`, children: "Unlink" })] }, child.id)))] }))] }), _jsxs(Row, { gap: "sm", wrap: true, children: [_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => setShowAddModal(true), children: "Add child listing" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestDissolveGroup, isLoading: loading, className: CLS_DELETE_LINK, children: "Dissolve group" })] })] })), !isGroupParent && groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", color: "muted", children: ["Part of:", " ", _jsx(Span, { weight: "medium", color: "primary", children: groupTitle ?? groupParentSlug })] }), _jsxs(Text, { size: "xs", color: "muted", children: ["Parent listing: ", _jsx(Code, { className: "font-mono", children: groupParentSlug })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestLeaveGroup, isLoading: loading, className: `${CLS_DELETE_LINK} w-fit`, children: "Leave group" })] }))] })), (() => {
267
+ }, paddingX: "none", paddingY: "xs", justify: "between", className: "w-full text-left group", "aria-expanded": open, children: [_jsx(Heading, { level: 3, size: "sm", weight: "semibold", color: "muted", children: "Group Settings" }), _jsx(Span, { size: "xs", className: "group-hover:text-[var(--appkit-color-text-muted)] dark:group-hover:text-[var(--appkit-color-text-faint)] transition-colors", color: "faint", children: open ? "▲" : "▼" })] }), open && (_jsxs(Stack, { gap: "sm", className: `mt-3 ${__P.p4}`, surface: "muted", rounded: "lg", border: "default", children: [!groupId && !groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", children: "Group related listings together \u2014 e.g. a set, bundle, or multi-part item. Parts can be sold individually but shown together." }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: startGroup, isLoading: loading, children: "Start a group" })] })), isGroupParent && groupId && (_jsxs(Stack, { gap: "md", children: [_jsxs(FormShellContext.Provider, { value: titleShellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: titleSections, values: titleDraft, onChange: (partial) => setTitleDraft((d) => ({ ...d, ...partial })), onSubmit: () => void saveTitle(), onValidationChange: () => titleForm.validate(titleDraft), schema: groupTitleSchema, openIds: titleNav.openIds, onOpenChange: titleNav.setOpenIds, submitLabel: "Save title", isLoading: loading })] }), _jsxs(Div, { children: [_jsxs(Text, { className: "mb-2", color: "muted", size: "xs", weight: "semibold", children: ["Members (", childSlugsCount + 1, " \u2014 including this listing)"] }), loading && !children ? (_jsx(Text, { size: "xs", color: "faint", children: "Loading\u2026" })) : (_jsxs(Div, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", children: [_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [_jsx(Span, { padding: "pill-2xs", weight: "semibold", className: "bg-[var(--appkit-color-primary)]/10 text-[var(--appkit-color-primary)] text-[10px]", rounded: "default", children: "Parent" }), _jsx(Text, { className: "flex-1", color: "primary", size: "sm", children: productSlug })] }), (children ?? []).filter((c) => c.id !== productId).map((child) => (_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [child.images?.[0] ? (_jsx(Div, { className: "relative w-8 h-8 flex-shrink-0 border border-[var(--appkit-color-border)]", rounded: "full", overflow: "hidden", children: _jsx(MediaImage, { src: child.images[0], alt: child.title, size: "thumbnail" }) })) : (_jsx(Div, { className: "w-8 h-8", surface: "subtle", rounded: "full" })), _jsx(Text, { className: "flex-1 truncate", color: "primary", size: "sm", children: child.title }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => requestUnlinkChild(child.id), isLoading: loading, className: `${CLS_DELETE_LINK} text-[length:var(--appkit-text-xs)]`, children: "Unlink" })] }, child.id)))] }))] }), _jsxs(Row, { gap: "sm", wrap: true, children: [_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => setShowAddModal(true), children: "Add child listing" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestDissolveGroup, isLoading: loading, className: CLS_DELETE_LINK, children: "Dissolve group" })] })] })), !isGroupParent && groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", color: "muted", children: ["Part of:", " ", _jsx(Span, { weight: "medium", color: "primary", children: groupTitle ?? groupParentSlug })] }), _jsxs(Text, { size: "xs", color: "muted", children: ["Parent listing: ", _jsx(Code, { className: "font-mono", children: groupParentSlug })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestLeaveGroup, isLoading: loading, className: `${CLS_DELETE_LINK} w-fit`, children: "Leave group" })] }))] })), (() => {
241
268
  const content = (_jsx(AddChildContent, { addTab: addTab, setAddTab: setAddTab, createForm: createForm, setCreateForm: setCreateForm, productSlug: productSlug, storeProductsEndpoint: storeProductsEndpoint, linkTargets: linkTargets, setLinkTargets: setLinkTargets, onAddCreate: addCreateChild, onAddLink: addLinkChild, loading: loading }));
242
269
  const close = () => setShowAddModal(false);
243
270
  return (groupChildSlugs?.length ?? 0) >= 4 ? (_jsx(SideDrawer, { isOpen: showAddModal, onClose: close, title: "Add child listing", children: content })) : (_jsx(Modal, { open: showAddModal, onClose: close, title: "Add child listing", size: "lg", children: content }));
244
271
  })(), confirmAction && (_jsx(ConfirmDeleteModal, { isOpen: true, title: confirmAction.title, message: confirmAction.message, onConfirm: confirmAction.onConfirm, onClose: () => setConfirmAction(null), isDeleting: loading }))] }));
245
272
  }
246
273
  function AddChildContent({ addTab, setAddTab, createForm, setCreateForm, productSlug, storeProductsEndpoint, linkTargets, setLinkTargets, onAddCreate, onAddLink, loading, }) {
247
- const { shellCtx, validate } = useFormShellState(createChildSchema);
274
+ const sections = React.useMemo(() => buildSectionsFromSchema(createChildSchema, {
275
+ options: { condition: CONDITION_OPTIONS },
276
+ }), []);
277
+ const nav = useSectionFormNav(sections, createForm, { scope: "store:group-add-child" });
278
+ const form = useFormShellState(createChildSchema, {
279
+ sections: nav.sectionMeta,
280
+ onGoToSection: nav.goToSection,
281
+ fieldToSectionIndex: nav.fieldToSectionIndex,
282
+ });
283
+ const shellCtx = form.shellCtx;
248
284
  useEffect(() => {
249
- validate(createForm);
285
+ form.validate(createForm);
250
286
  // eslint-disable-next-line react-hooks/exhaustive-deps
251
- }, [createForm, validate]);
252
- return (_jsx(Stack, { gap: "md", children: _jsxs(Tabs, { value: addTab, onChange: (v) => setAddTab(v), children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "create", children: "Create new child" }), _jsx(TabsTrigger, { value: "link", children: "Link existing" })] }), _jsx(TabsContent, { value: "create", children: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Stack, { gap: "sm", className: "mt-4", children: [_jsx(FormField, { name: "title", label: "Title *", type: "text", value: createForm.title, onChange: (v) => setCreateForm({ ...createForm, title: v }), placeholder: `${productSlug}-part` }), _jsx(FormField, { name: "price", label: "Price (\u20B9) *", type: "number", value: createForm.price, onChange: (v) => setCreateForm({ ...createForm, price: v }), placeholder: "0" }), _jsx(Select, { label: "Condition", value: createForm.condition, onValueChange: (v) => setCreateForm({ ...createForm, condition: v }), options: CONDITION_OPTIONS }), _jsx(Text, { className: "dark:text-[var(--appkit-color-text-faint)]/80", color: "faint", size: "xs", children: "Other fields (category, brand, shipping, return policy) are inherited from this parent listing. Need more control? Edit the full listing after saving." }), _jsx(FormErrorSummary, {}), _jsx(Button, { type: "button", onClick: onAddCreate, isLoading: loading, disabled: !createForm.title || !createForm.price, children: "Create and link child" })] }) }) }), _jsx(TabsContent, { value: "link", children: _jsxs(Stack, { gap: "sm", className: "mt-4", children: [_jsx(Text, { size: "xs", color: "muted", children: "Search your existing products or pre-orders. Auctions cannot be linked. Select multiple to bulk-link." }), _jsx(ProductInlineSelect, { scope: "store", endpoint: storeProductsEndpoint, filters: sieveFilter(PRODUCT_FIELDS.LISTING_TYPE, SIEVE_OP.EQ, "standard"), multiple: true, value: linkTargets, onChange: setLinkTargets, placeholder: "Search listings\u2026" }), _jsx(Button, { type: "button", onClick: onAddLink, isLoading: loading, disabled: linkTargets.length === 0, children: linkTargets.length > 1 ? `Link ${linkTargets.length} to group` : "Link to group" })] }) })] }) }));
287
+ }, [createForm]);
288
+ return (_jsx(Stack, { gap: "md", children: _jsxs(Tabs, { value: addTab, onChange: (v) => setAddTab(v), children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "create", children: "Create new child" }), _jsx(TabsTrigger, { value: "link", children: "Link existing" })] }), _jsx(TabsContent, { value: "create", children: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Stack, { gap: "sm", className: "mt-4", children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: createForm, onChange: (partial) => setCreateForm({ ...createForm, ...partial }), onSubmit: () => {
289
+ // SectionForm scrolls to the first erroring section and then
290
+ // submits regardless, so the guard lives here.
291
+ form.clearErrors();
292
+ const parsed = createChildSchema.safeParse(visibleValues(createChildSchema, createForm));
293
+ if (!parsed.success) {
294
+ applyZodIssues(parsed.error.issues, form.setFieldError);
295
+ return;
296
+ }
297
+ onAddCreate();
298
+ }, onValidationChange: () => form.validate(createForm), schema: createChildSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, submitLabel: "Create and link child", isLoading: loading }), _jsx(Text, { className: "dark:text-[var(--appkit-color-text-faint)]/80", color: "faint", size: "xs", children: "Need more control? Edit the full listing after saving." })] }) }) }), _jsx(TabsContent, { value: "link", children: _jsxs(Stack, { gap: "sm", className: "mt-4", children: [_jsx(Text, { size: "xs", color: "muted", children: "Search your existing products or pre-orders. Auctions cannot be linked. Select multiple to bulk-link." }), _jsx(ProductInlineSelect, { scope: "store", endpoint: storeProductsEndpoint, filters: sieveFilter(PRODUCT_FIELDS.LISTING_TYPE, SIEVE_OP.EQ, "standard"), multiple: true, value: linkTargets, onChange: setLinkTargets, placeholder: "Search listings\u2026" }), _jsx(Button, { type: "button", onClick: onAddLink, isLoading: loading, disabled: linkTargets.length === 0, children: linkTargets.length > 1 ? `Link ${linkTargets.length} to group` : "Link to group" })] }) })] }) }));
253
299
  }
@@ -0,0 +1,18 @@
1
+ import type { ActionIndexEntry } from "./types";
2
+ export interface ConsolidatedTabSurface {
3
+ /** Stable id suffix — `admin:tab:<slug>`. */
4
+ slug: string;
5
+ label: string;
6
+ description: string;
7
+ keywords: string[];
8
+ /** The host page's route key, resolved by the caller. */
9
+ route: "storePayouts" | "storeShipping" | "storeFulfillment" | "storeStorefront" | "adminRoles" | "adminSettings";
10
+ tab: string;
11
+ portal: "admin" | "store";
12
+ sectionPath: string;
13
+ requiredPermission?: string;
14
+ }
15
+ export declare const CONSOLIDATED_TAB_SURFACES: readonly ConsolidatedTabSurface[];
16
+ /** The host page each surface now lives on. */
17
+ export type ConsolidatedTabHrefs = Record<ConsolidatedTabSurface["route"], string>;
18
+ export declare function deriveConsolidatedTabEntries(hrefs: ConsolidatedTabHrefs): ActionIndexEntry[];
@@ -0,0 +1,128 @@
1
+ /*
2
+ * WHY: W8's C2 folded six sibling pages into tabs on five hosts. Each absorbed
3
+ * page kept its path as a redirect shell, but its NAV ENTRY was deleted —
4
+ * which is what removed it from the action index, since every nav-derived
5
+ * entry exists only because its nav item does.
6
+ *
7
+ * Left there, "payout methods", "print centre" and "permissions catalogue"
8
+ * would each stop being findable in the command palette on the day they
9
+ * became easier to reach. `deepLink` is a field separate from `href`
10
+ * precisely for this case.
11
+ *
12
+ * WHAT: One entry per absorbed surface, deep-linked to its tab.
13
+ *
14
+ * ## 🛑 Why these are authored rather than derived
15
+ *
16
+ * Every other entry in this index is derived, and that is the rule the whole
17
+ * file set exists to keep. These cannot be: the thing they name no longer has
18
+ * a nav item, a route of its own, or any other list to fall out of. The tab
19
+ * arrays are the closest thing, and their labels are written for a strip of
20
+ * four characters' width ("Methods", "Queue", "Slug") rather than for someone
21
+ * searching — which is the same reason the settings entries are authored too.
22
+ *
23
+ * The `href` still points at a real page and the tab id is still validated
24
+ * against that page's own union, so neither half can rot silently.
25
+ *
26
+ * EXPORTS: deriveConsolidatedTabEntries, CONSOLIDATED_TAB_SURFACES
27
+ *
28
+ * @tag domain:search
29
+ * @tag layer:constants
30
+ * @tag pattern:registry
31
+ * @tag access:isomorphic
32
+ * @tag consumers:src/constants/action-index.ts
33
+ * @tag sideEffects:none
34
+ */
35
+ export const CONSOLIDATED_TAB_SURFACES = [
36
+ {
37
+ slug: "payout-methods",
38
+ label: "Payout methods",
39
+ description: "Where your money goes — bank account or UPI.",
40
+ keywords: ["bank account", "upi", "payment details", "where do i get paid"],
41
+ route: "storePayouts",
42
+ tab: "methods",
43
+ portal: "store",
44
+ sectionPath: "Seller › Finance › Payouts",
45
+ },
46
+ {
47
+ slug: "payout-settings",
48
+ label: "Payout settings",
49
+ description: "How often you get paid, and the minimum amount.",
50
+ keywords: ["schedule", "frequency", "threshold", "minimum payout"],
51
+ route: "storePayouts",
52
+ tab: "settings",
53
+ portal: "store",
54
+ sectionPath: "Seller › Finance › Payouts",
55
+ },
56
+ {
57
+ slug: "shipping-configs",
58
+ label: "Named shipping configs",
59
+ description: "Reusable shipping rules you can attach to a listing.",
60
+ keywords: ["presets", "rules", "delivery", "rates"],
61
+ route: "storeShipping",
62
+ tab: "configs",
63
+ portal: "store",
64
+ sectionPath: "Seller › Store › Shipping",
65
+ },
66
+ {
67
+ slug: "print-center",
68
+ label: "Print centre",
69
+ description: "Print labels, invoices and barcodes in a batch.",
70
+ keywords: ["labels", "invoice", "barcode", "packing slip", "print"],
71
+ route: "storeFulfillment",
72
+ tab: "print",
73
+ portal: "store",
74
+ sectionPath: "Seller › Store › Fulfillment",
75
+ },
76
+ {
77
+ slug: "store-slug",
78
+ label: "Store web address",
79
+ description: "The URL buyers reach your shop at.",
80
+ keywords: ["url", "link", "domain", "slug", "change my address"],
81
+ route: "storeStorefront",
82
+ tab: "slug",
83
+ portal: "store",
84
+ sectionPath: "Seller › Store › Storefront",
85
+ },
86
+ {
87
+ slug: "permissions-catalog",
88
+ label: "Permissions catalogue",
89
+ description: "Every permission string and what it unlocks.",
90
+ keywords: ["rbac", "access", "capabilities", "what does this permission do"],
91
+ route: "adminRoles",
92
+ tab: "permissions",
93
+ portal: "admin",
94
+ sectionPath: "Admin › System › Custom Roles",
95
+ requiredPermission: "roles:read",
96
+ },
97
+ {
98
+ slug: "nav-permissions",
99
+ label: "Navigation permissions",
100
+ description: "Turn individual nav entries on or off across the portals.",
101
+ keywords: ["menu access", "hide nav", "sidebar", "rbac"],
102
+ route: "adminSettings",
103
+ tab: "navigation",
104
+ portal: "admin",
105
+ sectionPath: "Admin › System › Permissions Toggles",
106
+ requiredPermission: "settings:write",
107
+ },
108
+ ];
109
+ export function deriveConsolidatedTabEntries(hrefs) {
110
+ return CONSOLIDATED_TAB_SURFACES.map((surface) => ({
111
+ id: `${surface.portal}:tab:${surface.slug}`,
112
+ kind: "setting",
113
+ portal: surface.portal,
114
+ label: surface.label,
115
+ description: surface.description,
116
+ keywords: surface.keywords,
117
+ href: hrefs[surface.route],
118
+ deepLink: `?tab=${surface.tab}`,
119
+ sectionPath: surface.sectionPath,
120
+ requiredPermission: surface.requiredPermission,
121
+ /*
122
+ * Same weight as a settings control, and for the same reason: someone
123
+ * searching "upi" wants the payout-methods tab, not the Payouts page that
124
+ * also matches on its own (now broader) keyword list.
125
+ */
126
+ weight: 20,
127
+ }));
128
+ }
@@ -41,7 +41,11 @@ export function SellerBundlesView({ editHrefTemplate, newHref }) {
41
41
  const config = {
42
42
  portal: "seller",
43
43
  title: "Bundles",
44
- searchPlaceholder: "Search bundles by name or slug…",
44
+ search: {
45
+ placeholder: "Search bundles by name or slug…",
46
+ // `/api/store/bundles` filters in memory on `b.name` and `b.slug`.
47
+ fields: ["name", "slug"],
48
+ },
45
49
  emptyLabel: "No bundles yet — create a bundle to group multiple of your products together",
46
50
  filterKeys: ["isActive", "bundleStockStatus"],
47
51
  defaultSort: sortBy("name", "ASC"),
@@ -1,22 +1,15 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { normalizeError } from "../../../errors/normalize";
4
- import { useCallback, useEffect, useState } from "react";
5
- import { z } from "zod";
4
+ import { useCallback, useEffect, useMemo, useState } from "react";
6
5
  import { RefreshCw } from "lucide-react";
7
- import { Alert, Button, Div, FormField, Heading, Row, Section, Stack, Text, Toggle, useToast } from "../../../ui";
6
+ import { Alert, Button, Div, Heading, Row, Section, Stack, Text, useToast } from "../../../ui";
8
7
  import { useFormShellState, FormShellContext, FormErrorSummary } from "../../../ui/forms";
8
+ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
9
+ import { SectionForm, useSectionFormNav, buildSectionsFromSchema, visibleValues } from "../../shell";
10
+ import { sellerGoogleReviewsFormSchema } from "../../store-extensions/schemas/google-config-form";
9
11
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
10
12
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
11
- const googleReviewsDraftSchema = z.object({
12
- isConnected: z.boolean(),
13
- placeId: z.string(),
14
- businessName: z.string(),
15
- }).superRefine((v, ctx) => {
16
- if (v.isConnected && !v.placeId.trim()) {
17
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["placeId"], message: "A Place ID is required to show Google reviews on your store page" });
18
- }
19
- });
20
13
  const EMPTY_DRAFT = {
21
14
  placeId: "",
22
15
  businessName: "",
@@ -29,11 +22,19 @@ export function SellerGoogleReviewsView({ onSave, onSync, }) {
29
22
  const [syncing, setSyncing] = useState(false);
30
23
  const [saveMessage, setSaveMessage] = useState(null);
31
24
  const { showToast } = useToast();
32
- const { shellCtx, validate } = useFormShellState(googleReviewsDraftSchema);
33
- useEffect(() => {
34
- validate(draft);
35
- // eslint-disable-next-line react-hooks/exhaustive-deps
36
- }, [draft, validate]);
25
+ /*
26
+ * Three fields, all derived. The stats below (average rating, total reviews,
27
+ * last sync) are SYNC OUTPUT, not configuration — the schema deliberately
28
+ * omits them, per its own header, so a seller cannot type the review count
29
+ * their storefront displays.
30
+ */
31
+ const sections = useMemo(() => buildSectionsFromSchema(sellerGoogleReviewsFormSchema), []);
32
+ const nav = useSectionFormNav(sections, draft, { scope: "seller:google-reviews" });
33
+ const { shellCtx, setFieldError, clearErrors, validate } = useFormShellState(sellerGoogleReviewsFormSchema, {
34
+ sections: nav.sectionMeta,
35
+ onGoToSection: nav.goToSection,
36
+ fieldToSectionIndex: nav.fieldToSectionIndex,
37
+ });
37
38
  useEffect(() => {
38
39
  fetch(SELLER_ENDPOINTS.GOOGLE_REVIEWS, { credentials: "include" })
39
40
  .then((r) => r.json())
@@ -52,6 +53,17 @@ export function SellerGoogleReviewsView({ onSave, onSync, }) {
52
53
  .finally(() => setLoading(false));
53
54
  }, []);
54
55
  const handleSave = useCallback(async () => {
56
+ /*
57
+ * 🛑 The guard is here because `SectionForm.handleSubmit` scrolls to the
58
+ * first erroring section and then calls `onSubmit()` UNCONDITIONALLY — it
59
+ * surfaces errors, it does not block on them.
60
+ */
61
+ clearErrors();
62
+ const parsed = sellerGoogleReviewsFormSchema.safeParse(visibleValues(sellerGoogleReviewsFormSchema, draft));
63
+ if (!parsed.success) {
64
+ applyZodIssues(parsed.error.issues, setFieldError);
65
+ return;
66
+ }
55
67
  setSaving(true);
56
68
  setSaveMessage(null);
57
69
  try {
@@ -81,7 +93,7 @@ export function SellerGoogleReviewsView({ onSave, onSync, }) {
81
93
  finally {
82
94
  setSaving(false);
83
95
  }
84
- }, [draft, onSave, showToast]);
96
+ }, [draft, onSave, showToast, clearErrors, setFieldError]);
85
97
  const handleSync = useCallback(async () => {
86
98
  setSyncing(true);
87
99
  setSaveMessage(null);
@@ -116,5 +128,5 @@ export function SellerGoogleReviewsView({ onSave, onSync, }) {
116
128
  timeStyle: "short",
117
129
  })
118
130
  : "Never";
119
- return (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(Div, { paddingX: "x-sm-md", className: "max-w-2xl", padding: "y-md", children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Section, { children: [_jsx(Heading, { level: 3, className: "mb-1", children: "Google Business Settings" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] mb-4", size: "sm", children: "Connect your Google Business profile to sync reviews to your storefront." }), _jsxs(Stack, { gap: "md", children: [_jsx(Toggle, { checked: draft.isConnected, onChange: (v) => setDraft((d) => ({ ...d, isConnected: v })), label: "Show Google reviews on my store page" }), _jsx(FormField, { name: "placeId", label: "Google Place ID", type: "text", value: draft.placeId, onChange: (v) => setDraft((d) => ({ ...d, placeId: v })), placeholder: "ChIJ\u2026", helpText: "Find your Place ID at developers.google.com/maps/documentation/places/web-service/place-id" }), _jsx(FormField, { name: "businessName", label: "Business name", type: "text", value: draft.businessName, onChange: (v) => setDraft((d) => ({ ...d, businessName: v })), placeholder: "Pok\u00E9mon Palace" })] })] }), (draft.averageRating !== undefined || draft.totalReviews !== undefined) && (_jsxs(Section, { children: [_jsx(Heading, { level: 3, className: "mb-3", children: "Review Stats" }), _jsxs(Row, { wrap: true, gap: "lg", children: [draft.averageRating !== undefined && (_jsxs(Div, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)] tracking-wide", size: "xs", transform: "uppercase", children: "Avg rating" }), _jsx(Text, { className: "tabular-nums", size: "2xl", weight: "bold", children: draft.averageRating.toFixed(1) })] })), draft.totalReviews !== undefined && (_jsxs(Div, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)] tracking-wide", size: "xs", transform: "uppercase", children: "Total reviews" }), _jsx(Text, { className: "tabular-nums", size: "2xl", weight: "bold", children: draft.totalReviews })] })), _jsxs(Div, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)] tracking-wide", size: "xs", transform: "uppercase", children: "Last synced" }), _jsx(Text, { size: "sm", children: formattedLastSync })] })] })] })), saveMessage && (_jsx(Alert, { variant: saveMessage.ok ? "success" : "error", children: saveMessage.text })), _jsx(FormErrorSummary, {}), _jsxs(Row, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", align: "center", justify: "between", gap: "3", children: [_jsxs(Button, { gap: "sm", variant: "outline", size: "sm", onClick: handleSync, isLoading: syncing, disabled: saving || !draft.placeId.trim(), children: [_jsx(RefreshCw, { className: "h-3.5 w-3.5" }), ACTIONS.STORE["google-reviews-sync"].label] }), _jsx(Button, { variant: "primary", size: "sm", onClick: handleSave, isLoading: saving, disabled: syncing, children: ACTIONS.STORE["save-google-settings"].label })] })] }) }) }));
131
+ return (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(Div, { paddingX: "x-sm-md", className: "max-w-2xl", padding: "y-md", children: _jsxs(Stack, { gap: "lg", children: [(draft.averageRating !== undefined || draft.totalReviews !== undefined) && (_jsxs(Section, { children: [_jsx(Heading, { level: 3, className: "mb-3", children: "Review Stats" }), _jsxs(Row, { wrap: true, gap: "lg", children: [draft.averageRating !== undefined && (_jsxs(Div, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)] tracking-wide", size: "xs", transform: "uppercase", children: "Avg rating" }), _jsx(Text, { className: "tabular-nums", size: "2xl", weight: "bold", children: draft.averageRating.toFixed(1) })] })), draft.totalReviews !== undefined && (_jsxs(Div, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)] tracking-wide", size: "xs", transform: "uppercase", children: "Total reviews" }), _jsx(Text, { className: "tabular-nums", size: "2xl", weight: "bold", children: draft.totalReviews })] })), _jsxs(Div, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)] tracking-wide", size: "xs", transform: "uppercase", children: "Last synced" }), _jsx(Text, { size: "sm", children: formattedLastSync })] })] })] })), saveMessage && (_jsx(Alert, { variant: saveMessage.ok ? "success" : "error", children: saveMessage.text })), _jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: draft, onChange: (partial) => setDraft((d) => ({ ...d, ...partial })), onSubmit: handleSave, onValidationChange: () => validate(draft), schema: sellerGoogleReviewsFormSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, submitLabel: ACTIONS.STORE["save-google-settings"].label, isLoading: saving }), _jsx(Row, { padding: "t-md", className: "border-t border-[var(--appkit-color-border)]", align: "center", justify: "start", children: _jsxs(Button, { gap: "sm", variant: "outline", size: "sm", onClick: handleSync, isLoading: syncing, disabled: saving || !(draft.placeId ?? "").trim(), children: [_jsx(RefreshCw, { className: "h-3.5 w-3.5" }), ACTIONS.STORE["google-reviews-sync"].label] }) })] }) }) }));
120
132
  }
@@ -8,13 +8,18 @@ import React, { useState, useCallback } from "react";
8
8
  import { Eye, ExternalLink, Printer, MapPin, Truck } from "lucide-react";
9
9
  import { useActionDispatch } from "../../../react/hooks/use-action-dispatch";
10
10
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
11
- import { Badge, Button, Div, FilterChipGroup, Heading, Input, Select, SideDrawer, Span, Stack, Text, useToast } from "../../../ui";
11
+ import { Badge, Button, Div, FilterChipGroup, SideDrawer, Span, Stack, Text, useToast } from "../../../ui";
12
12
  import { SELLER_ORDER_STATUS_TABS } from "../../admin/constants/filter-tabs";
13
13
  import { useOrderScope } from "../../orders/components/OrderScopeTabs";
14
14
  import { OrderStatusValues } from "../../orders/schemas/firestore";
15
15
  import { isManualPaymentMethod } from "../../orders/constants/payment-window";
16
16
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
17
17
  import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers";
18
+ import { sellerOrderUpdateSchema, } from "../schemas/order-forms";
19
+ import { SectionForm, useSectionFormNav, buildSectionsFromSchema, visibleValues, } from "../../shell";
20
+ import { useFormShellState, FormShellContext } from "../../../ui/forms";
21
+ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
22
+ import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
18
23
  import { PhysicalLocationModal } from "./PhysicalLocationModal";
19
24
  import { ROUTES } from "../../../constants";
20
25
  import { toRecordArray, toRelativeDate, toCurrency, toStringValue } from "../hooks/useSellerListingData";
@@ -88,13 +93,21 @@ export function SellerOrderDetailPanel({ orderId, apiBase, onClose, }) {
88
93
  const [fetchError, setFetchError] = useState(null);
89
94
  const [saving, setSaving] = useState(false);
90
95
  const [saveError, setSaveError] = useState(null);
91
- const [newStatus, setNewStatus] = useState("");
92
- const [trackingNumber, setTrackingNumber] = useState("");
93
- const [carrier, setCarrier] = useState("");
94
- const [trackingUrl, setTrackingUrl] = useState("");
96
+ const [draft, setDraft] = useState({
97
+ status: "", trackingNumber: "", carrier: "", trackingUrl: "",
98
+ });
95
99
  const [markingPaidIndex, setMarkingPaidIndex] = useState(null);
96
100
  const [emiError, setEmiError] = useState(null);
97
101
  const { showToast } = useToast();
102
+ const sections = React.useMemo(() => buildSectionsFromSchema(sellerOrderUpdateSchema, {
103
+ options: { status: UPDATE_STATUS_OPTIONS },
104
+ }), []);
105
+ const nav = useSectionFormNav(sections, draft, { scope: "store:order-update" });
106
+ const form = useFormShellState(sellerOrderUpdateSchema, {
107
+ sections: nav.sectionMeta,
108
+ onGoToSection: nav.goToSection,
109
+ fieldToSectionIndex: nav.fieldToSectionIndex,
110
+ });
98
111
  React.useEffect(() => {
99
112
  setLoading(true);
100
113
  setFetchError(null);
@@ -103,9 +116,12 @@ export function SellerOrderDetailPanel({ orderId, apiBase, onClose, }) {
103
116
  .then((json) => {
104
117
  const o = (json?.data ?? json);
105
118
  setOrder(o);
106
- setTrackingNumber(o.trackingNumber ?? "");
107
- setCarrier(o.carrier ?? "");
108
- setTrackingUrl(o.trackingUrl ?? "");
119
+ setDraft({
120
+ status: "",
121
+ trackingNumber: o.trackingNumber ?? "",
122
+ carrier: o.carrier ?? "",
123
+ trackingUrl: o.trackingUrl ?? "",
124
+ });
109
125
  })
110
126
  .catch(() => setFetchError("Failed to load order details"))
111
127
  .finally(() => setLoading(false));
@@ -141,15 +157,19 @@ export function SellerOrderDetailPanel({ orderId, apiBase, onClose, }) {
141
157
  setSaving(true);
142
158
  setSaveError(null);
143
159
  try {
160
+ const v = visibleValues(sellerOrderUpdateSchema, draft);
144
161
  const payload = {};
145
- if (newStatus)
146
- payload.status = newStatus;
147
- if (trackingNumber !== (order.trackingNumber ?? ""))
148
- payload.trackingNumber = trackingNumber;
149
- if (carrier !== (order.carrier ?? ""))
150
- payload.shippingCarrier = carrier;
151
- if (trackingUrl !== (order.trackingUrl ?? ""))
152
- payload.trackingUrl = trackingUrl;
162
+ // A DIFF, not a snapshot: the panel writes only what the seller changed,
163
+ // so re-saving an untouched field cannot clobber a value another path
164
+ // (the shipping provider, a support agent) has since written.
165
+ if (v.status)
166
+ payload.status = v.status;
167
+ if ((v.trackingNumber ?? "") !== (order.trackingNumber ?? ""))
168
+ payload.trackingNumber = v.trackingNumber ?? "";
169
+ if ((v.carrier ?? "") !== (order.carrier ?? ""))
170
+ payload.shippingCarrier = v.carrier ?? "";
171
+ if ((v.trackingUrl ?? "") !== (order.trackingUrl ?? ""))
172
+ payload.trackingUrl = v.trackingUrl ?? "";
153
173
  if (Object.keys(payload).length === 0) {
154
174
  onClose();
155
175
  return;
@@ -164,8 +184,16 @@ export function SellerOrderDetailPanel({ orderId, apiBase, onClose, }) {
164
184
  throw new Error(d?.error ?? "Failed to update order");
165
185
  }
166
186
  const updated = await res.json();
167
- setOrder((updated?.data ?? updated));
168
- setNewStatus("");
187
+ const next = (updated?.data ?? updated);
188
+ setOrder(next);
189
+ // Reset to "keep current" and re-seed the shipment fields from what the
190
+ // server actually stored, so the next diff is against the saved state.
191
+ setDraft({
192
+ status: "",
193
+ trackingNumber: next.trackingNumber ?? "",
194
+ carrier: next.carrier ?? "",
195
+ trackingUrl: next.trackingUrl ?? "",
196
+ });
169
197
  }
170
198
  catch (err) {
171
199
  void normalizeError(err);
@@ -181,7 +209,17 @@ export function SellerOrderDetailPanel({ orderId, apiBase, onClose, }) {
181
209
  ? `Paid ${toRelativeDate(inst.paidAt)}`
182
210
  : inst.dueDate
183
211
  ? `Due ${toRelativeDate(inst.dueDate)}`
184
- : "" })] }), _jsxs(Row, { align: "center", gap: "sm", className: "shrink-0", children: [_jsx(Badge, { variant: EMI_INSTALLMENT_BADGE_VARIANT[inst.status] ?? "default", children: inst.status }), inst.status !== "paid" && (_jsx(Button, { action: ACTIONS.STORE["mark-installment-paid"], size: "sm", variant: "outline", isLoading: markingPaidIndex === inst.index, disabled: markingPaidIndex !== null, onClick: () => handleMarkInstallmentPaid(inst.index), children: "Mark paid" }))] })] }, inst.index))) }), emiError && (_jsx(Div, { textSize: "xs", className: "mt-2 border border-error/20", color: "error", surface: "danger-surface", padding: "inlineSm", rounded: "lg", children: emiError }))] })), _jsxs(Stack, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", gap: "3", children: [_jsx(Heading, { level: 4, size: "sm", weight: "semibold", children: "Update order" }), _jsx(Select, { label: "New status", value: newStatus, options: UPDATE_STATUS_OPTIONS, onChange: (e) => setNewStatus(e.target.value) }), _jsx(Input, { label: "Tracking number", value: trackingNumber, onChange: (e) => setTrackingNumber(e.target.value), placeholder: "e.g. 12345678901234" }), _jsx(Input, { label: "Carrier", value: carrier, onChange: (e) => setCarrier(e.target.value), placeholder: "e.g. Delhivery, Bluedart" }), _jsx(Input, { label: "Tracking URL (optional)", value: trackingUrl, onChange: (e) => setTrackingUrl(e.target.value), placeholder: "https://...", type: "url" }), saveError && (_jsx(Div, { textSize: "xs", className: "border border-error/20", color: "error", surface: "danger-surface", padding: "inlineSm", rounded: "lg", children: saveError }))] })] }), _jsxs(Row, { border: "top", paddingY: "y-sm-tall", padding: "x-md", align: "center", justify: "end", gap: "3", children: [_jsx(Button, { variant: "outline", onClick: onClose, disabled: saving, children: "Close" }), _jsx(Button, { onClick: handleSave, isLoading: saving, disabled: saving, children: "Save" })] })] }))] }));
212
+ : "" })] }), _jsxs(Row, { align: "center", gap: "sm", className: "shrink-0", children: [_jsx(Badge, { variant: EMI_INSTALLMENT_BADGE_VARIANT[inst.status] ?? "default", children: inst.status }), inst.status !== "paid" && (_jsx(Button, { action: ACTIONS.STORE["mark-installment-paid"], size: "sm", variant: "outline", isLoading: markingPaidIndex === inst.index, disabled: markingPaidIndex !== null, onClick: () => handleMarkInstallmentPaid(inst.index), children: "Mark paid" }))] })] }, inst.index))) }), emiError && (_jsx(Div, { textSize: "xs", className: "mt-2 border border-error/20", color: "error", surface: "danger-surface", padding: "inlineSm", rounded: "lg", children: emiError }))] })), _jsxs(Stack, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", gap: "3", children: [_jsxs(FormShellContext.Provider, { value: form.shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: draft, onChange: (partial) => setDraft((d) => ({ ...d, ...partial })), onSubmit: () => {
213
+ // SectionForm scrolls to the first erroring section and then
214
+ // submits regardless, so the guard lives here.
215
+ form.clearErrors();
216
+ const parsed = sellerOrderUpdateSchema.safeParse(visibleValues(sellerOrderUpdateSchema, draft));
217
+ if (!parsed.success) {
218
+ applyZodIssues(parsed.error.issues, form.setFieldError);
219
+ return;
220
+ }
221
+ void handleSave();
222
+ }, onValidationChange: () => form.validate(draft), schema: sellerOrderUpdateSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, submitLabel: "Save", cancelLabel: "Close", onCancel: onClose, isLoading: saving })] }), saveError && (_jsx(Div, { textSize: "xs", className: "border border-error/20", color: "error", surface: "danger-surface", padding: "inlineSm", rounded: "lg", children: saveError }))] })] }), _jsxs(Row, { border: "top", paddingY: "y-sm-tall", padding: "x-md", align: "center", justify: "end", gap: "3", children: [_jsx(Button, { variant: "outline", onClick: onClose, disabled: saving, children: "Close" }), _jsx(Button, { onClick: handleSave, isLoading: saving, disabled: saving, children: "Save" })] })] }))] }));
185
223
  }
186
224
  function OrderDetailDrawer({ orderId, apiBase, onClose, }) {
187
225
  return (_jsx(SideDrawer, { isOpen: true, title: `Order ${orderId}`, onClose: onClose, children: _jsx(SellerOrderDetailPanel, { orderId: orderId, apiBase: apiBase, onClose: onClose }) }));
@@ -212,7 +212,16 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
212
212
  const config = {
213
213
  portal: "seller",
214
214
  title: "Products",
215
- searchPlaceholder: "Search products by name…",
215
+ search: {
216
+ placeholder: "Search by name, description, brand or tag…",
217
+ /*
218
+ * The box understated itself: `/api/store/products` routes `q` through
219
+ * `buildProductSearchTxt`, which indexes far more than the title. A
220
+ * seller searching a brand or a tag got results and had no reason to
221
+ * expect them.
222
+ */
223
+ fields: ["title", "description", "brand", "categoryNames", "tags", "features"],
224
+ },
216
225
  emptyLabel: listingKind !== "all" ? `No ${listingKind} listings found` : "No products listed yet",
217
226
  filterKeys: FILTER_KEYS,
218
227
  defaultSort: DEFAULT_SORT,
@@ -1,5 +1,6 @@
1
+ import React from "react";
1
2
  export interface SellerReviewsViewProps {
2
3
  reviewsApiBase?: string;
3
4
  replyApiBase?: string;
4
5
  }
5
- export declare function SellerReviewsView({ reviewsApiBase, replyApiBase, }: SellerReviewsViewProps): import("react").JSX.Element;
6
+ export declare function SellerReviewsView({ reviewsApiBase, replyApiBase, }: SellerReviewsViewProps): React.JSX.Element;