@mohasinac/appkit 4.31.0 → 4.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/products/list-public.d.ts +18 -0
- package/dist/_internal/server/features/products/list-public.js +27 -9
- package/dist/client.d.ts +9 -0
- package/dist/client.js +12 -0
- package/dist/features/admin/components/AdminAdsView.js +36 -34
- package/dist/features/admin/components/AdminCarouselEditorView.js +78 -10
- package/dist/features/admin/components/AdminOrderEditorView.js +55 -24
- package/dist/features/admin/components/AdminProductEditorView.js +78 -18
- package/dist/features/admin/components/AdminSectionsView.js +89 -32
- package/dist/features/admin/components/AdminSiteSettingsView.js +96 -44
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +58 -43
- package/dist/features/admin/components/AdminUserEditorView.js +65 -82
- package/dist/features/admin/constants/admin-page-tabs.d.ts +36 -0
- package/dist/features/admin/constants/admin-page-tabs.js +45 -0
- package/dist/features/admin/schemas/admin-ops-forms.d.ts +81 -0
- package/dist/features/admin/schemas/admin-ops-forms.js +123 -1
- package/dist/features/admin/schemas/admin-user-form.d.ts +78 -3
- package/dist/features/admin/schemas/admin-user-form.js +96 -7
- package/dist/features/admin/schemas/homepage-section-form.d.ts +9 -0
- package/dist/features/layout/PageTabs.d.ts +17 -0
- package/dist/features/layout/PageTabs.js +45 -0
- package/dist/features/products/components/GroupSettingsPanel.js +62 -16
- package/dist/features/search/action-index/tab-entries.d.ts +18 -0
- package/dist/features/search/action-index/tab-entries.js +128 -0
- package/dist/features/seller/components/SellerGoogleReviewsView.js +31 -19
- package/dist/features/seller/components/SellerOrdersView.js +57 -19
- package/dist/features/seller/components/SellerReviewsView.d.ts +2 -1
- package/dist/features/seller/components/SellerReviewsView.js +54 -19
- package/dist/features/seller/constants/store-tabs.d.ts +64 -0
- package/dist/features/seller/constants/store-tabs.js +60 -0
- package/dist/features/seller/schemas/order-forms.d.ts +33 -0
- package/dist/features/seller/schemas/order-forms.js +75 -0
- package/dist/features/seller/schemas/review-forms.d.ts +37 -0
- package/dist/features/seller/schemas/review-forms.js +82 -0
- package/dist/features/shell/index.d.ts +1 -1
- package/dist/features/shell/index.js +5 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +44 -15
- package/dist/features/shipments/schemas/validation.d.ts +41 -0
- package/dist/features/shipments/schemas/validation.js +49 -0
- package/dist/features/store-extensions/schemas/google-config-form.d.ts +36 -0
- package/dist/features/store-extensions/schemas/google-config-form.js +24 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +67 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +53 -22
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/next/index.d.ts +8 -0
- package/dist/next/index.js +37 -0
- package/dist/react/hooks/useTabParam.d.ts +19 -0
- package/dist/react/hooks/useTabParam.js +65 -0
- package/dist/schemas/registry.d.ts +27 -3
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -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 }) }));
|
|
@@ -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;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize";
|
|
4
|
-
import { useCallback, useState } from "react";
|
|
4
|
+
import React, { useCallback, useState } from "react";
|
|
5
|
+
import { reviewReplySchema, reviewBulkReplySchema, reviewContestSchema, reviewFeedbackSchema, } from "../schemas/review-forms";
|
|
6
|
+
import { SectionForm, useSectionFormNav, buildSectionsFromSchema, visibleValues, } from "../../shell";
|
|
7
|
+
import { useFormShellState, FormShellContext } from "../../../ui/forms";
|
|
8
|
+
import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
|
|
9
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
5
10
|
import { useQueryClient } from "@tanstack/react-query";
|
|
6
|
-
import { Alert, Badge, Button, Checkbox, Div, Modal, Row, Select, SideDrawer, Span, Stack, Text,
|
|
11
|
+
import { Alert, Badge, Button, Checkbox, Div, Modal, Row, Select, SideDrawer, Span, Stack, Text, useToast } from "../../../ui";
|
|
7
12
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
8
13
|
import { SELLER_BULK_ACTIONS, ROW_ACTION_META } from "../../products/constants/action-defs";
|
|
9
14
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
@@ -32,28 +37,58 @@ function statusBadge(status) {
|
|
|
32
37
|
};
|
|
33
38
|
return _jsx(Badge, { variant: map[status] ?? "default", children: status });
|
|
34
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The four write surfaces on this page are each ONE textarea, and four
|
|
42
|
+
* copies of the same twenty lines of SectionForm wiring is exactly the Rule of
|
|
43
|
+
* Three trigger. It stays local: the shape is "a single-field modal form", not
|
|
44
|
+
* a generally useful primitive, and `QuickFormDrawer` already owns the
|
|
45
|
+
* generalised version for drawers.
|
|
46
|
+
*
|
|
47
|
+
* `hideActions` is deliberately NOT used — each modal's own footer buttons are
|
|
48
|
+
* removed in favour of this row, so there is one Save and one Cancel per form.
|
|
49
|
+
*/
|
|
50
|
+
function ReviewTextForm({ schema, values, onChange, onSubmit, submitLabel, onCancel, isLoading, scope, }) {
|
|
51
|
+
const sections = React.useMemo(() => buildSectionsFromSchema(schema), [schema]);
|
|
52
|
+
const nav = useSectionFormNav(sections, values, { scope });
|
|
53
|
+
const form = useFormShellState(schema, {
|
|
54
|
+
sections: nav.sectionMeta,
|
|
55
|
+
onGoToSection: nav.goToSection,
|
|
56
|
+
fieldToSectionIndex: nav.fieldToSectionIndex,
|
|
57
|
+
});
|
|
58
|
+
return (_jsxs(FormShellContext.Provider, { value: form.shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: values, onChange: onChange, onSubmit: () => {
|
|
59
|
+
// SectionForm scrolls to the first erroring section and then submits
|
|
60
|
+
// regardless, so the guard lives here.
|
|
61
|
+
form.clearErrors();
|
|
62
|
+
const parsed = schema.safeParse(visibleValues(schema, values));
|
|
63
|
+
if (!parsed.success) {
|
|
64
|
+
applyZodIssues(parsed.error.issues, form.setFieldError);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
onSubmit();
|
|
68
|
+
}, onValidationChange: () => form.validate(values), schema: schema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, submitLabel: submitLabel, cancelLabel: "Cancel", onCancel: onCancel, isLoading: isLoading })] }));
|
|
69
|
+
}
|
|
35
70
|
export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, replyApiBase = SELLER_ENDPOINTS.REVIEWS, }) {
|
|
36
71
|
const queryClient = useQueryClient();
|
|
37
72
|
const { showToast } = useToast();
|
|
38
73
|
const refetchReviews = useCallback(() => queryClient.invalidateQueries({ queryKey: ["seller", "reviews", "listing"] }), [queryClient]);
|
|
39
74
|
// Reply drawer
|
|
40
75
|
const [replyTarget, setReplyTarget] = useState(null);
|
|
41
|
-
const [
|
|
76
|
+
const [replyDraft, setReplyDraft] = useState({ reply: "" });
|
|
42
77
|
const [replySaving, setReplySaving] = useState(false);
|
|
43
78
|
const [replyError, setReplyError] = useState(null);
|
|
44
79
|
// S-STORE-4-C — bulk reply + contest + feedback selection
|
|
45
80
|
const [bulkReplyOpen, setBulkReplyOpen] = useState(false);
|
|
46
|
-
const [
|
|
81
|
+
const [bulkDraft, setBulkDraft] = useState({ reply: "" });
|
|
47
82
|
const [bulkSaving, setBulkSaving] = useState(false);
|
|
48
83
|
const [bulkTargetIds, setBulkTargetIds] = useState([]);
|
|
49
84
|
const [bulkClearSelection, setBulkClearSelection] = useState(null);
|
|
50
85
|
const [contestTarget, setContestTarget] = useState(null);
|
|
51
|
-
const [
|
|
86
|
+
const [contestDraft, setContestDraft] = useState({ reason: "" });
|
|
52
87
|
const [feedbackTarget, setFeedbackTarget] = useState(null);
|
|
53
|
-
const [
|
|
88
|
+
const [feedbackDraft, setFeedbackDraft] = useState({ feedback: "" });
|
|
54
89
|
const openReply = (review) => {
|
|
55
90
|
setReplyTarget(review);
|
|
56
|
-
|
|
91
|
+
setReplyDraft({ reply: review.sellerReply ?? "" });
|
|
57
92
|
setReplyError(null);
|
|
58
93
|
};
|
|
59
94
|
const handleReplySave = async (onDone) => {
|
|
@@ -65,7 +100,7 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
65
100
|
const res = await fetch(`${replyApiBase}/${replyTarget.id}/reply`, {
|
|
66
101
|
method: "POST",
|
|
67
102
|
headers: { "Content-Type": "application/json" },
|
|
68
|
-
body: JSON.stringify({ reply:
|
|
103
|
+
body: JSON.stringify({ reply: replyDraft.reply }),
|
|
69
104
|
});
|
|
70
105
|
const json = await res.json();
|
|
71
106
|
if (!res.ok)
|
|
@@ -91,7 +126,7 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
91
126
|
const res = await fetch(SELLER_ENDPOINTS.REVIEW_CONTEST(contestTarget.id), {
|
|
92
127
|
method: "POST",
|
|
93
128
|
headers: { "Content-Type": "application/json" },
|
|
94
|
-
body: JSON.stringify({ reason:
|
|
129
|
+
body: JSON.stringify({ reason: contestDraft.reason }),
|
|
95
130
|
});
|
|
96
131
|
if (!res.ok)
|
|
97
132
|
throw new Error(`Could not submit the contest (${res.status})`);
|
|
@@ -101,9 +136,9 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
101
136
|
return;
|
|
102
137
|
}
|
|
103
138
|
setContestTarget(null);
|
|
104
|
-
|
|
139
|
+
setContestDraft({ reason: "" });
|
|
105
140
|
onDone();
|
|
106
|
-
}, [contestTarget,
|
|
141
|
+
}, [contestTarget, contestDraft, showToast]);
|
|
107
142
|
const submitFeedback = useCallback(async () => {
|
|
108
143
|
if (!feedbackTarget)
|
|
109
144
|
return;
|
|
@@ -111,7 +146,7 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
111
146
|
const res = await fetch(SELLER_ENDPOINTS.REVIEW_FEEDBACK(feedbackTarget.id), {
|
|
112
147
|
method: "POST",
|
|
113
148
|
headers: { "Content-Type": "application/json" },
|
|
114
|
-
body: JSON.stringify({ feedback:
|
|
149
|
+
body: JSON.stringify({ feedback: feedbackDraft.feedback }),
|
|
115
150
|
});
|
|
116
151
|
if (!res.ok)
|
|
117
152
|
throw new Error(`Could not send the feedback (${res.status})`);
|
|
@@ -121,10 +156,10 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
121
156
|
return;
|
|
122
157
|
}
|
|
123
158
|
setFeedbackTarget(null);
|
|
124
|
-
|
|
125
|
-
}, [feedbackTarget,
|
|
159
|
+
setFeedbackDraft({ feedback: "" });
|
|
160
|
+
}, [feedbackTarget, feedbackDraft, showToast]);
|
|
126
161
|
const submitBulkReply = useCallback(async () => {
|
|
127
|
-
if (!
|
|
162
|
+
if (!bulkDraft.reply.trim() || bulkTargetIds.length === 0)
|
|
128
163
|
return;
|
|
129
164
|
setBulkSaving(true);
|
|
130
165
|
// Partial success is the normal outcome of a bulk write, so report the
|
|
@@ -132,7 +167,7 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
132
167
|
const outcomes = await Promise.all(bulkTargetIds.map((id) => fetch(`${replyApiBase}/${id}/reply`, {
|
|
133
168
|
method: "POST",
|
|
134
169
|
headers: { "Content-Type": "application/json" },
|
|
135
|
-
body: JSON.stringify({ reply:
|
|
170
|
+
body: JSON.stringify({ reply: bulkDraft.reply }),
|
|
136
171
|
})
|
|
137
172
|
.then((res) => res.ok)
|
|
138
173
|
.catch((err) => {
|
|
@@ -145,11 +180,11 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
145
180
|
}
|
|
146
181
|
setBulkSaving(false);
|
|
147
182
|
setBulkReplyOpen(false);
|
|
148
|
-
|
|
183
|
+
setBulkDraft({ reply: "" });
|
|
149
184
|
bulkClearSelection?.();
|
|
150
185
|
setBulkTargetIds([]);
|
|
151
186
|
await refetchReviews();
|
|
152
|
-
}, [
|
|
187
|
+
}, [bulkDraft, bulkTargetIds, replyApiBase, bulkClearSelection, refetchReviews, showToast]);
|
|
153
188
|
const config = {
|
|
154
189
|
portal: "seller",
|
|
155
190
|
title: "Reviews",
|
|
@@ -193,5 +228,5 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
193
228
|
})),
|
|
194
229
|
renderCards: (rows, _view, selection, isLoading) => (_jsx(Stack, { gap: "md", children: isLoading ? (_jsx(Div, { className: "text-center", padding: "y-xl", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", children: "Loading reviews\u2026" }) })) : rows.length === 0 ? (_jsx(Div, { className: "text-center", padding: "y-3xl", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", children: "No reviews found." }) })) : (rows.map((review) => (_jsx(Div, { className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "lg", padding: "md", children: _jsxs(Row, { align: "start", justify: "between", gap: "3", wrap: true, children: [_jsxs(Row, { align: "start", className: "flex-1 min-w-0", gap: "3", children: [_jsx(Checkbox, { checked: selection.selectedIds.includes(review.id), onChange: () => selection.toggleSelect(review.id), "aria-label": "Select review" }), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { className: "truncate", weight: "medium", children: review.productTitle }), _jsxs(Row, { className: "mt-1", align: "center", gap: "sm", wrap: true, children: [_jsx(Stars, { rating: review.rating }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: ["by ", review.userName] }), review.verified && _jsx(Badge, { variant: "success", children: "Verified" }), statusBadge(review.status), _jsx(Badge, { variant: review.sellerReply ? "success" : "warning", children: review.sellerReply ? "Store replied" : "Awaiting store reply" })] }), review.title && _jsx(Text, { className: "mt-2", weight: "medium", children: review.title }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-secondary)] line-clamp-3", size: "sm", children: review.comment }), review.sellerReply && (_jsxs(Div, { className: "mt-2 pl-[0.75rem] border-l-2 border-[var(--appkit-color-primary)]", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "Store reply:" }), _jsx(Text, { size: "sm", children: review.sellerReply })] }))] })] }), _jsxs(Row, { className: "flex-shrink-0", gap: "sm", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => openReply(review), children: review.sellerReply ? "Edit Reply" : ACTIONS.STORE["reply-review"].label }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setContestTarget(review), children: ACTIONS.STORE["contest-review"].label }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setFeedbackTarget(review), children: ACTIONS.STORE["buyer-feedback"].label })] })] }) }, review.id)))) })),
|
|
195
230
|
};
|
|
196
|
-
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(SideDrawer, { isOpen: !!replyTarget, onClose: () => setReplyTarget(null), title: replyTarget?.sellerReply ? "Edit Reply" : "Reply to Review", mode: "create", children: _jsxs(Stack, { gap: "md", className: `${__P.p4}`, children: [replyTarget && (_jsxs(Div, { className: `${__P.p3} bg-[var(--appkit-color-surface-muted)]`, rounded: "default", children: [_jsx(Stars, { rating: replyTarget.rating }), _jsx(Text, { className: "mt-1", size: "sm", children: replyTarget.comment })] })), replyError && _jsx(Alert, { variant: "error", children: replyError }),
|
|
231
|
+
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(SideDrawer, { isOpen: !!replyTarget, onClose: () => setReplyTarget(null), title: replyTarget?.sellerReply ? "Edit Reply" : "Reply to Review", mode: "create", children: _jsxs(Stack, { gap: "md", className: `${__P.p4}`, children: [replyTarget && (_jsxs(Div, { className: `${__P.p3} bg-[var(--appkit-color-surface-muted)]`, rounded: "default", children: [_jsx(Stars, { rating: replyTarget.rating }), _jsx(Text, { className: "mt-1", size: "sm", children: replyTarget.comment })] })), replyError && _jsx(Alert, { variant: "error", children: replyError }), _jsx(ReviewTextForm, { schema: reviewReplySchema, values: replyDraft, onChange: (partial) => setReplyDraft((d) => ({ ...d, ...partial })), onSubmit: () => void handleReplySave(refetchReviews), submitLabel: replyTarget?.sellerReply ? "Update Reply" : "Post Reply", onCancel: () => setReplyTarget(null), isLoading: replySaving, scope: "store:review-reply" })] }) }), _jsx(Modal, { isOpen: bulkReplyOpen, onClose: () => setBulkReplyOpen(false), title: "Bulk reply to selected reviews", children: _jsx(Stack, { gap: "md", padding: "2xs", children: _jsx(ReviewTextForm, { schema: reviewBulkReplySchema, values: bulkDraft, onChange: (partial) => setBulkDraft((d) => ({ ...d, ...partial })), onSubmit: () => void submitBulkReply(), submitLabel: "Send reply", onCancel: () => setBulkReplyOpen(false), isLoading: bulkSaving, scope: "store:review-bulk-reply" }) }) }), _jsx(Modal, { isOpen: !!contestTarget, onClose: () => setContestTarget(null), title: "Contest this review", children: _jsxs(Stack, { gap: "md", padding: "2xs", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Flag this review for admin investigation." }), _jsx(ReviewTextForm, { schema: reviewContestSchema, values: contestDraft, onChange: (partial) => setContestDraft((d) => ({ ...d, ...partial })), onSubmit: () => void submitContest(refetchReviews), submitLabel: "Submit", onCancel: () => setContestTarget(null), scope: "store:review-contest" })] }) }), _jsx(Modal, { isOpen: !!feedbackTarget, onClose: () => setFeedbackTarget(null), title: "Send feedback to buyer", children: _jsx(Stack, { gap: "md", padding: "2xs", children: _jsx(ReviewTextForm, { schema: reviewFeedbackSchema, values: feedbackDraft, onChange: (partial) => setFeedbackDraft((d) => ({ ...d, ...partial })), onSubmit: () => void submitFeedback(), submitLabel: "Send", onCancel: () => setFeedbackTarget(null), scope: "store:review-feedback" }) }) })] }));
|
|
197
232
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🛑 `StoreDashboardTab`, not `StoreTab` — that name is already taken by
|
|
3
|
+
* `features/stores`, which uses it for a tab on the PUBLIC store detail page.
|
|
4
|
+
* Two same-named types in one package is the defect Root Cause #36 records, and
|
|
5
|
+
* the barrel's choice between them would be invisible at every call site.
|
|
6
|
+
*/
|
|
7
|
+
export interface StoreDashboardTab {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
/** What lives in here, for anyone who does not know the tab's name. */
|
|
11
|
+
description: string;
|
|
12
|
+
}
|
|
13
|
+
/** "Get paid": what I am owed, where it goes, on what schedule. */
|
|
14
|
+
export declare const PAYOUTS_TABS: readonly [{
|
|
15
|
+
readonly id: "payouts";
|
|
16
|
+
readonly label: "Payouts";
|
|
17
|
+
readonly description: "What you have been paid, and what is still due.";
|
|
18
|
+
}, {
|
|
19
|
+
readonly id: "methods";
|
|
20
|
+
readonly label: "Methods";
|
|
21
|
+
readonly description: "Where your money goes — bank or UPI.";
|
|
22
|
+
}, {
|
|
23
|
+
readonly id: "settings";
|
|
24
|
+
readonly label: "Settings";
|
|
25
|
+
readonly description: "How often you get paid, and the minimum.";
|
|
26
|
+
}];
|
|
27
|
+
export type PayoutsTabId = (typeof PAYOUTS_TABS)[number]["id"];
|
|
28
|
+
export declare function isPayoutsTabId(value: string | null | undefined): value is PayoutsTabId;
|
|
29
|
+
/** Shipping defaults, plus the reusable named presets built on top of them. */
|
|
30
|
+
export declare const SHIPPING_TABS: readonly [{
|
|
31
|
+
readonly id: "rates";
|
|
32
|
+
readonly label: "Rates & zones";
|
|
33
|
+
readonly description: "What you charge to ship, and where you ship to.";
|
|
34
|
+
}, {
|
|
35
|
+
readonly id: "configs";
|
|
36
|
+
readonly label: "Named configs";
|
|
37
|
+
readonly description: "Named shipping rules you can reuse per listing.";
|
|
38
|
+
}];
|
|
39
|
+
export type ShippingTabId = (typeof SHIPPING_TABS)[number]["id"];
|
|
40
|
+
export declare function isShippingTabId(value: string | null | undefined): value is ShippingTabId;
|
|
41
|
+
/** The pack-and-dispatch loop: the queue, then the paperwork it needs. */
|
|
42
|
+
export declare const FULFILLMENT_TABS: readonly [{
|
|
43
|
+
readonly id: "queue";
|
|
44
|
+
readonly label: "Queue";
|
|
45
|
+
readonly description: "Orders waiting to be packed or handed over.";
|
|
46
|
+
}, {
|
|
47
|
+
readonly id: "print";
|
|
48
|
+
readonly label: "Print centre";
|
|
49
|
+
readonly description: "Print labels, invoices and barcodes in a batch.";
|
|
50
|
+
}];
|
|
51
|
+
export type FulfillmentTabId = (typeof FULFILLMENT_TABS)[number]["id"];
|
|
52
|
+
export declare function isFulfillmentTabId(value: string | null | undefined): value is FulfillmentTabId;
|
|
53
|
+
/** The shop's identity — its look, and the address buyers reach it at. */
|
|
54
|
+
export declare const STOREFRONT_TABS: readonly [{
|
|
55
|
+
readonly id: "profile";
|
|
56
|
+
readonly label: "Profile";
|
|
57
|
+
readonly description: "Your shop's name, logo, banner and description.";
|
|
58
|
+
}, {
|
|
59
|
+
readonly id: "slug";
|
|
60
|
+
readonly label: "Web address";
|
|
61
|
+
readonly description: "The web address buyers reach your shop at.";
|
|
62
|
+
}];
|
|
63
|
+
export type StorefrontTabId = (typeof STOREFRONT_TABS)[number]["id"];
|
|
64
|
+
export declare function isStorefrontTabId(value: string | null | undefined): value is StorefrontTabId;
|