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