@mohasinac/appkit 4.31.1 → 4.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/products/list-public.d.ts +18 -0
- package/dist/_internal/server/features/products/list-public.js +27 -9
- package/dist/client.d.ts +9 -0
- package/dist/client.js +12 -0
- package/dist/features/account/components/UserBidsView.js +6 -1
- package/dist/features/account/components/UserOrdersView.js +8 -1
- package/dist/features/account/components/UserReturnsView.js +6 -1
- package/dist/features/admin/components/AdminAdsView.js +36 -34
- package/dist/features/admin/components/AdminAuditLogView.js +12 -1
- package/dist/features/admin/components/AdminBundlesView.js +5 -1
- package/dist/features/admin/components/AdminCarouselEditorView.js +78 -10
- package/dist/features/admin/components/AdminFaqsView.js +10 -1
- package/dist/features/admin/components/AdminNotificationsView.js +7 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +55 -24
- package/dist/features/admin/components/AdminPrizeDrawsView.js +11 -1
- package/dist/features/admin/components/AdminProductEditorView.js +78 -18
- package/dist/features/admin/components/AdminSectionsView.js +89 -32
- package/dist/features/admin/components/AdminSiteSettingsView.js +96 -44
- package/dist/features/admin/components/AdminStoresView.js +13 -1
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +58 -43
- package/dist/features/admin/components/AdminSupportTicketsView.js +11 -1
- package/dist/features/admin/components/AdminTeamView.js +11 -1
- package/dist/features/admin/components/AdminTesterChecklistView.js +6 -1
- package/dist/features/admin/components/AdminUserEditorView.js +65 -82
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.js +6 -1
- package/dist/features/admin/constants/admin-page-tabs.d.ts +36 -0
- package/dist/features/admin/constants/admin-page-tabs.js +45 -0
- package/dist/features/admin/schemas/admin-ops-forms.d.ts +81 -0
- package/dist/features/admin/schemas/admin-ops-forms.js +123 -1
- package/dist/features/admin/schemas/admin-user-form.d.ts +78 -3
- package/dist/features/admin/schemas/admin-user-form.js +96 -7
- package/dist/features/admin/schemas/homepage-section-form.d.ts +9 -0
- package/dist/features/layout/PageTabs.d.ts +17 -0
- package/dist/features/layout/PageTabs.js +45 -0
- package/dist/features/products/components/GroupSettingsPanel.js +62 -16
- package/dist/features/search/action-index/tab-entries.d.ts +18 -0
- package/dist/features/search/action-index/tab-entries.js +128 -0
- package/dist/features/seller/components/SellerBundlesView.js +5 -1
- package/dist/features/seller/components/SellerGoogleReviewsView.js +31 -19
- package/dist/features/seller/components/SellerOrdersView.js +57 -19
- package/dist/features/seller/components/SellerProductsView.js +10 -1
- package/dist/features/seller/components/SellerReviewsView.d.ts +2 -1
- package/dist/features/seller/components/SellerReviewsView.js +54 -19
- package/dist/features/seller/constants/store-tabs.d.ts +64 -0
- package/dist/features/seller/constants/store-tabs.js +60 -0
- package/dist/features/seller/schemas/order-forms.d.ts +33 -0
- package/dist/features/seller/schemas/order-forms.js +75 -0
- package/dist/features/seller/schemas/review-forms.d.ts +37 -0
- package/dist/features/seller/schemas/review-forms.js +82 -0
- package/dist/features/shell/index.d.ts +1 -1
- package/dist/features/shell/index.js +5 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +44 -15
- package/dist/features/shipments/schemas/validation.d.ts +41 -0
- package/dist/features/shipments/schemas/validation.js +49 -0
- package/dist/features/store-extensions/schemas/google-config-form.d.ts +36 -0
- package/dist/features/store-extensions/schemas/google-config-form.js +24 -0
- package/dist/features/stores/actions/store-query-actions.js +5 -3
- package/dist/features/stores/repository/store.repository.d.ts +3 -1
- package/dist/features/stores/repository/store.repository.js +6 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +79 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +53 -22
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.js +1 -0
- package/dist/react/hooks/useTabParam.d.ts +19 -0
- package/dist/react/hooks/useTabParam.js +65 -0
- package/dist/schemas/registry.d.ts +27 -3
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,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,
|
|
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
|
-
|
|
20
|
-
|
|
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 [
|
|
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
|
|
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
|
-
|
|
77
|
+
titleForm.validate(titleDraft);
|
|
51
78
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
52
|
-
}, [
|
|
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,
|
|
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: [
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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: [
|
|
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,
|
|
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 [
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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: [
|
|
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
|
-
|
|
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):
|
|
6
|
+
export declare function SellerReviewsView({ reviewsApiBase, replyApiBase, }: SellerReviewsViewProps): React.JSX.Element;
|