@mohasinac/appkit 4.60.1 → 4.62.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/.build-stamp.json +3 -3
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +19 -3
- package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +1 -1
- package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +1 -1
- package/dist/_internal/server/features/auctions/actions.d.ts +1 -1
- package/dist/_internal/server/features/auctions/actions.js +12 -1
- package/dist/_internal/server/features/blog/actions.d.ts +1 -1
- package/dist/_internal/server/features/blog/actions.js +12 -1
- package/dist/_internal/server/features/brands/actions.d.ts +1 -1
- package/dist/_internal/server/features/brands/actions.js +12 -1
- package/dist/_internal/server/features/cart/actions.d.ts +1 -1
- package/dist/_internal/server/features/cart/actions.js +12 -1
- package/dist/_internal/server/features/checkout/actions.js +0 -1
- package/dist/_internal/server/features/events/actions.d.ts +1 -1
- package/dist/_internal/server/features/events/actions.js +12 -1
- package/dist/_internal/server/features/gateway-events/redact.js +14 -2
- package/dist/_internal/server/features/history/actions.d.ts +1 -1
- package/dist/_internal/server/features/history/actions.js +12 -1
- package/dist/_internal/server/features/lottery/actions.d.ts +1 -1
- package/dist/_internal/server/features/lottery/actions.js +12 -1
- package/dist/_internal/server/features/orders/actions.d.ts +1 -1
- package/dist/_internal/server/features/orders/actions.js +12 -1
- package/dist/_internal/server/features/orders/cancellation-refund.js +0 -1
- package/dist/_internal/server/features/orders/service.js +31 -1
- package/dist/_internal/server/features/payouts/actions.d.ts +1 -1
- package/dist/_internal/server/features/payouts/actions.js +12 -1
- package/dist/_internal/server/features/points/award.js +9 -1
- package/dist/_internal/server/features/pre-orders/actions.d.ts +1 -1
- package/dist/_internal/server/features/pre-orders/actions.js +12 -1
- package/dist/_internal/server/features/prize-draws/actions.d.ts +1 -1
- package/dist/_internal/server/features/prize-draws/actions.js +12 -1
- package/dist/_internal/server/features/products/actions.d.ts +1 -1
- package/dist/_internal/server/features/products/actions.js +12 -1
- package/dist/_internal/server/features/promotions/actions.d.ts +1 -1
- package/dist/_internal/server/features/promotions/actions.js +12 -1
- package/dist/_internal/server/features/protection-claims/actions.d.ts +2 -2
- package/dist/_internal/server/features/protection-claims/actions.js +21 -12
- package/dist/_internal/server/features/raffle/actions.d.ts +1 -1
- package/dist/_internal/server/features/raffle/actions.js +12 -1
- package/dist/_internal/server/features/refunds/actions.d.ts +1 -3
- package/dist/_internal/server/features/refunds/actions.js +14 -2
- package/dist/_internal/server/features/reviews/actions.d.ts +1 -1
- package/dist/_internal/server/features/reviews/actions.js +12 -1
- package/dist/_internal/server/features/wishlist/actions.d.ts +1 -1
- package/dist/_internal/server/features/wishlist/actions.js +12 -1
- package/dist/_internal/server/jobs/core/obligationSweep.js +0 -1
- package/dist/_internal/server/jobs/core/paymentReconcile.js +93 -76
- package/dist/_internal/server/jobs/core/testDataCleanup.d.ts +1 -1
- package/dist/_internal/server/jobs/core/testDataCleanup.js +22 -1
- package/dist/_internal/server/jobs/handlers/onShipmentAllocationSync.js +16 -4
- package/dist/_internal/shared/actions/action-registry.js +149 -9
- package/dist/_internal/shared/features/promotions/schema.d.ts +66 -0
- package/dist/_internal/shared/features/promotions/schema.js +7 -0
- package/dist/_internal/shared/media/limits.d.ts +10 -0
- package/dist/_internal/shared/media/limits.js +10 -0
- package/dist/_internal/shared/obligations/deadlines.js +11 -1
- package/dist/_internal/shared/tester/window.d.ts +25 -0
- package/dist/_internal/shared/tester/window.js +66 -0
- package/dist/_internal/shared/types/action-result.js +15 -1
- package/dist/client/api/surface-error.js +16 -2
- package/dist/client/api/useApiMutation.js +17 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/constants/api-endpoints.d.ts +3 -0
- package/dist/constants/api-endpoints.js +1 -0
- package/dist/constants/field-names.d.ts +17 -0
- package/dist/constants/field-names.js +23 -0
- package/dist/core/server-action.js +19 -3
- package/dist/core/unit-of-work.js +15 -1
- package/dist/errors/error-mapping.js +26 -1
- package/dist/errors/firestore-codes.d.ts +37 -0
- package/dist/errors/firestore-codes.js +52 -0
- package/dist/errors/index.d.ts +0 -1
- package/dist/errors/index.js +12 -2
- package/dist/features/account/components/UserOffersPanel.js +5 -3
- package/dist/features/admin/components/AdminActionIndexView.js +100 -5
- package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
- package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
- package/dist/features/admin/components/AdminCarouselView.js +96 -4
- package/dist/features/admin/components/AdminCouponEditorView.js +8 -0
- package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
- package/dist/features/admin/components/AdminGatewayEventsView.js +2 -2
- package/dist/features/admin/components/AdminMediaView.js +36 -3
- package/dist/features/admin/components/AdminNavigationView.js +4 -1
- package/dist/features/admin/components/AdminOffersView.js +2 -1
- package/dist/features/admin/components/AdminPaymentClustersView.js +19 -2
- package/dist/features/admin/components/AdminProductEditorView.js +2 -2
- package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
- package/dist/features/admin/components/AdminSectionsView.js +486 -224
- package/dist/features/admin/components/DataListingView.js +2 -0
- package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
- package/dist/features/admin/components/analytics/AdminStatCard.js +4 -1
- package/dist/features/admin/hooks/useOptimisticRowFlag.d.ts +33 -0
- package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
- package/dist/features/admin/repository/notification.repository.d.ts +15 -0
- package/dist/features/admin/repository/notification.repository.js +32 -0
- package/dist/features/admin/repository/site-settings.repository.d.ts +2 -1
- package/dist/features/admin/repository/site-settings.repository.js +7 -0
- package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
- package/dist/features/admin/schemas/action-index-form.js +115 -0
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +1 -1
- package/dist/features/auth/components/RegisterForm.js +2 -1
- package/dist/features/catalogue/actions/approve-catalogue-listing.d.ts +1 -1
- package/dist/features/catalogue/actions/approve-catalogue-listing.js +12 -1
- package/dist/features/catalogue/actions/list-from-catalogue.d.ts +1 -1
- package/dist/features/catalogue/actions/list-from-catalogue.js +12 -1
- package/dist/features/catalogue/actions/submit-for-approval.d.ts +1 -1
- package/dist/features/catalogue/actions/submit-for-approval.js +12 -1
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
- package/dist/features/categories/repository/price-observation.repository.d.ts +19 -0
- package/dist/features/categories/repository/price-observation.repository.js +21 -2
- package/dist/features/events/actions/event-actions.js +4 -1
- package/dist/features/events/components/AdminEventEditorView.js +79 -50
- package/dist/features/events/components/EventsListPageView.js +2 -1
- package/dist/features/events/schemas/firestore.d.ts +17 -0
- package/dist/features/events/schemas/firestore.js +1 -0
- package/dist/features/events/utils/sort-pinned.d.ts +33 -0
- package/dist/features/events/utils/sort-pinned.js +46 -0
- package/dist/features/gateway-events/repository/gateway-events.repository.js +3 -40
- package/dist/features/gateway-events/schemas/index.d.ts +2 -2
- package/dist/features/inventory/components/AdminStorageBoxesView.js +55 -39
- package/dist/features/inventory/components/ReceiveStationView.js +61 -17
- package/dist/features/inventory/repository/storage-boxes.repository.d.ts +3 -1
- package/dist/features/inventory/repository/storage-boxes.repository.js +36 -3
- package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
- package/dist/features/obligations/repository/obligations.repository.js +10 -1
- package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
- package/dist/features/orders/components/RefundRequestView.js +4 -4
- package/dist/features/orders/schemas/firestore.d.ts +19 -0
- package/dist/features/orders/schemas/firestore.js +19 -0
- package/dist/features/orders/schemas/index.d.ts +6 -6
- package/dist/features/payment-cases/repository/payment-cases.repository.d.ts +1 -0
- package/dist/features/payment-cases/repository/payment-cases.repository.js +3 -14
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
- package/dist/features/products/components/GroupMemberPicker.js +4 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +1 -1
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
- package/dist/features/products/components/ProductDetailActions.js +2 -1
- package/dist/features/products/components/ProductForm.js +2 -2
- package/dist/features/products/components/ProductGrid.js +1 -1
- package/dist/features/promotions/repository/claimed-coupons.repository.js +31 -1
- package/dist/features/promotions/schemas/firestore.d.ts +22 -0
- package/dist/features/promotions/schemas/firestore.js +22 -0
- package/dist/features/promotions/schemas/index.d.ts +479 -6
- package/dist/features/promotions/schemas/index.js +153 -0
- package/dist/features/protection-claims/repository/protection-claims.repository.js +22 -15
- package/dist/features/seller/components/FulfillmentView.js +3 -2
- package/dist/features/seller/components/SellerAddressesView.js +32 -4
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
- package/dist/features/seller/components/SellerBundlesView.js +53 -14
- package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
- package/dist/features/seller/components/SellerListingTypeView.js +56 -10
- package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
- package/dist/features/seller/components/SellerProductsView.js +5 -1
- package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
- package/dist/features/seller/components/SellerShippingView.js +2 -1
- package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
- package/dist/features/seller/components/SellerSupportView.js +69 -2
- package/dist/features/seller/schemas/coupon-form.d.ts +5 -0
- package/dist/features/seller/schemas/coupon-form.js +18 -0
- package/dist/features/shell/SectionForm.js +35 -9
- package/dist/features/shell/build-sections.js +14 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
- package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
- package/dist/features/sticker-trace/components/StickerCommitPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
- package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
- package/dist/features/sticker-trace/server.d.ts +2 -0
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
- package/dist/http/api-handler.js +4 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +6 -4
- package/dist/next/routing/route-map.d.ts +4 -2
- package/dist/next/routing/route-map.js +19 -1
- package/dist/schemas/registry.d.ts +15 -10
- package/dist/schemas/sieve.d.ts +3 -3
- package/dist/server.d.ts +1 -2
- package/dist/server.js +6 -4
- package/dist/styles.css +51 -0
- package/dist/ui/components/Checkbox.js +10 -2
- package/dist/ui/components/DateInput.js +8 -1
- package/dist/ui/components/Input.js +22 -2
- package/dist/ui/components/PaginatedSelect.d.ts +8 -19
- package/dist/ui/components/PaginatedSelect.js +0 -0
- package/dist/ui/components/Radio.js +8 -1
- package/dist/ui/components/Select.js +10 -2
- package/dist/ui/components/TagInput.d.ts +8 -1
- package/dist/ui/components/TagInput.js +4 -3
- package/dist/ui/components/Textarea.js +10 -2
- package/dist/ui/forms/FieldCheckbox.js +1 -1
- package/dist/ui/forms/FormErrorList.d.ts +0 -16
- package/dist/ui/forms/FormErrorList.js +34 -1
- package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
- package/dist/ui/forms/apply-zod-issues.js +23 -1
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +1822 -1812
- package/dist/features/tester/utils/phases.d.ts +0 -48
- package/dist/features/tester/utils/phases.js +0 -139
|
@@ -37,6 +37,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
37
37
|
*/
|
|
38
38
|
import React, { useCallback, useState } from "react";
|
|
39
39
|
import { Badge, ConfirmDeleteModal, RowActionMenu, Span, Text } from "../../../ui/index.js";
|
|
40
|
+
import { apiClient } from "../../../http/index.js";
|
|
40
41
|
import { useEntityDelete } from "../../../react/hooks/useEntityDelete.js";
|
|
41
42
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
42
43
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
@@ -46,6 +47,7 @@ import { PRODUCT_FIELDS } from "../../../constants/field-names.js";
|
|
|
46
47
|
import { SIEVE_OP, sieveFilter } from "../../../utils/sieve-builder.js";
|
|
47
48
|
import { pluginFor } from "../../../_internal/shared/listing-types/_registry.js";
|
|
48
49
|
import { toCurrency, toRecordArray, toRelativeDate, toStringValue, } from "../../admin/hooks/useAdminListingData.js";
|
|
50
|
+
import { useOptimisticRowFlag } from "../../admin/hooks/useOptimisticRowFlag.js";
|
|
49
51
|
import { DataListingView } from "../../admin/components/DataListingView.js";
|
|
50
52
|
import { ROW_ACTION_META, SELLER_BULK_ACTIONS } from "../../products/constants/action-defs.js";
|
|
51
53
|
import { useAvailabilityScope } from "../../products/hooks/useAvailabilityScope.js";
|
|
@@ -204,6 +206,17 @@ export function SellerListingTypeView({ type, onCreateClick, onEditClick, onDele
|
|
|
204
206
|
await performDelete(id);
|
|
205
207
|
setDeleteTargetId(null);
|
|
206
208
|
}, [performDelete]);
|
|
209
|
+
/*
|
|
210
|
+
* Optimistic status, via the shared hook — see its header for why a
|
|
211
|
+
* rollback must DELETE the override key rather than write back a captured
|
|
212
|
+
* value, and why the key is cleared on success too.
|
|
213
|
+
*/
|
|
214
|
+
const statusFlag = useOptimisticRowFlag({
|
|
215
|
+
mutate: (id, next) => apiClient.patch(SELLER_ENDPOINTS.PRODUCT_BY_ID(id), { status: next }),
|
|
216
|
+
invalidate: [["seller", plugin.tabSlug]],
|
|
217
|
+
successMessage: (next) => next === "published" ? "Listing published." : "Listing unpublished.",
|
|
218
|
+
errorMessage: "Could not change that listing's status.",
|
|
219
|
+
});
|
|
207
220
|
const handleEdit = useCallback((id) => {
|
|
208
221
|
if (onEditClick)
|
|
209
222
|
onEditClick(id);
|
|
@@ -233,7 +246,15 @@ export function SellerListingTypeView({ type, onCreateClick, onEditClick, onDele
|
|
|
233
246
|
{
|
|
234
247
|
key: "status",
|
|
235
248
|
header: "Status",
|
|
236
|
-
|
|
249
|
+
/*
|
|
250
|
+
* Reads the optimistic override so the badge and the row action agree
|
|
251
|
+
* the instant one is used — otherwise the menu would say "Unpublish"
|
|
252
|
+
* while the badge beside it still said published, until the refetch.
|
|
253
|
+
*/
|
|
254
|
+
render: (row) => {
|
|
255
|
+
const status = statusFlag.resolve(row.id, row.status);
|
|
256
|
+
return (_jsx(Badge, { variant: status === "active" || status === "published" ? "active" : "inactive", size: "xs", className: "capitalize", children: status }));
|
|
257
|
+
},
|
|
237
258
|
},
|
|
238
259
|
textCell("createdAt", spec.createdHeader ?? "Created"),
|
|
239
260
|
];
|
|
@@ -291,15 +312,40 @@ export function SellerListingTypeView({ type, onCreateClick, onEditClick, onDele
|
|
|
291
312
|
},
|
|
292
313
|
}))
|
|
293
314
|
: undefined,
|
|
294
|
-
renderRowActions: (row) =>
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
label: ACTIONS.STORE["
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
315
|
+
renderRowActions: (row) => {
|
|
316
|
+
const isPublished = statusFlag.resolve(row.id, row.status) === "published";
|
|
317
|
+
return (_jsx(RowActionMenu, { actions: [
|
|
318
|
+
{ label: ACTIONS.STORE["edit-listing"].label, onClick: () => handleEdit(row.id) },
|
|
319
|
+
/*
|
|
320
|
+
* 🛑 This one entry lights up SIX seller pages — art, classified,
|
|
321
|
+
* digital codes, live, spares and stickers are each a ~25-line
|
|
322
|
+
* wrapper around this component.
|
|
323
|
+
*
|
|
324
|
+
* `status` was a read-only Badge on all of them, so changing one
|
|
325
|
+
* listing from published to draft meant opening the full editor,
|
|
326
|
+
* even though `PATCH /api/store/products/[id]` has always taken
|
|
327
|
+
* `status` and SellerProductsView already drives that exact call.
|
|
328
|
+
*
|
|
329
|
+
* `ACTIONS.STORE["publish-listing"]` / `["unpublish-listing"]`
|
|
330
|
+
* were defined with ZERO call sites — this is what finally wires
|
|
331
|
+
* them, rather than reinventing the labels inline.
|
|
332
|
+
*/
|
|
333
|
+
{
|
|
334
|
+
label: isPublished
|
|
335
|
+
? ACTIONS.STORE["unpublish-listing"].label
|
|
336
|
+
: ACTIONS.STORE["publish-listing"].label,
|
|
337
|
+
separator: true,
|
|
338
|
+
disabled: statusFlag.pendingId === row.id,
|
|
339
|
+
onClick: () => { void statusFlag.set(row.id, isPublished ? "draft" : "published"); },
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
label: ACTIONS.STORE["delete-listing"].label,
|
|
343
|
+
destructive: true,
|
|
344
|
+
onClick: () => setDeleteTargetId(row.id),
|
|
345
|
+
disabled: deletingId === row.id,
|
|
346
|
+
},
|
|
347
|
+
] }));
|
|
348
|
+
},
|
|
303
349
|
};
|
|
304
350
|
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), deleteTargetId && (_jsx(ConfirmDeleteModal, { isOpen: true, title: `Delete ${spec.deleteLabel}`, message: `Are you sure you want to delete this ${spec.deleteLabel.toLowerCase()}? This cannot be undone.`, onConfirm: () => handleDelete(deleteTargetId), onClose: () => setDeleteTargetId(null), isDeleting: deletingId === deleteTargetId }))] }));
|
|
305
351
|
}
|
|
@@ -8,6 +8,8 @@ import { Badge, ConfirmDeleteModal, Div, Row, RowActionMenu, Span, Text, useToas
|
|
|
8
8
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
9
9
|
import { ROUTES } from "../../../constants/index.js";
|
|
10
10
|
import { toRecordArray, toStringValue } from "../hooks/useSellerListingData.js";
|
|
11
|
+
import { useOptimisticRowFlag } from "../../admin/hooks/useOptimisticRowFlag.js";
|
|
12
|
+
import { apiClient } from "../../../http/index.js";
|
|
11
13
|
import { DataListingView } from "../../admin/components/DataListingView.js";
|
|
12
14
|
const CLS_TYPE_PILL = "inline-flex items-center rounded-full px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-semibold uppercase tracking-wide bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-300";
|
|
13
15
|
const DEFAULT_SORT = "-createdAt";
|
|
@@ -42,6 +44,22 @@ const COLUMNS = [
|
|
|
42
44
|
},
|
|
43
45
|
];
|
|
44
46
|
export function SellerPayoutMethodsView({ onCreateClick, onEditClick, onDelete, onSetDefault, }) {
|
|
47
|
+
/*
|
|
48
|
+
* 🛑 `isActive` was a READ-ONLY badge here, and the update schema was built
|
|
49
|
+
* to take it on its own — shipping-config's even says so in a comment: "a
|
|
50
|
+
* PATCH may legitimately carry only `isActive`". Nothing in the client had
|
|
51
|
+
* ever sent one, so flipping one boolean meant a full-editor round trip.
|
|
52
|
+
*
|
|
53
|
+
* The optimistic/rollback logic lives in the hook because eight listings
|
|
54
|
+
* grew this control at once; see its header for why rollback must DELETE
|
|
55
|
+
* the override key rather than write back a captured value.
|
|
56
|
+
*/
|
|
57
|
+
const activeFlag = useOptimisticRowFlag({
|
|
58
|
+
mutate: (id, next) => apiClient.patch(SELLER_ENDPOINTS.PAYOUT_METHOD_BY_ID(id), { isActive: next }),
|
|
59
|
+
invalidate: [["seller", "payout-methods", "listing"]],
|
|
60
|
+
successMessage: (next) => (next ? "Payout method activated." : "Payout method deactivated."),
|
|
61
|
+
errorMessage: "Could not change that payout method's status.",
|
|
62
|
+
});
|
|
45
63
|
const [deleteTargetId, setDeleteTargetId] = useState(null);
|
|
46
64
|
const { deletingId, handleDelete: performDelete } = useEntityDelete({
|
|
47
65
|
endpoint: SELLER_ENDPOINTS.PAYOUT_METHOD_BY_ID,
|
|
@@ -114,13 +132,27 @@ export function SellerPayoutMethodsView({ onCreateClick, onEditClick, onDelete,
|
|
|
114
132
|
label: String(item.label ?? ""),
|
|
115
133
|
type: String(item.type ?? ""),
|
|
116
134
|
isDefault: Boolean(item.isDefault),
|
|
117
|
-
|
|
135
|
+
// Resolved through the pending override so the Status badge and the
|
|
136
|
+
// row toggle cannot disagree between the click and the refetch.
|
|
137
|
+
isActive: activeFlag.resolve(toStringValue(item.id, `pm-${index}`), Boolean(item.isActive)),
|
|
118
138
|
maskedIdentifier: getMaskedIdentifier(item),
|
|
119
139
|
})),
|
|
120
140
|
getTotal: (response, mappedRows) => (typeof response.total === "number" ? response.total : mappedRows.length),
|
|
121
141
|
buildFilters: () => undefined,
|
|
122
142
|
primaryAction: { label: "New Method", onClick: handleCreate },
|
|
123
143
|
renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
|
|
144
|
+
/*
|
|
145
|
+
* 🛑 `isActive` was a read-only badge and the update schema was
|
|
146
|
+
* built to accept it alone. A menu entry rather than a switch here,
|
|
147
|
+
* to match the other actions in this menu — the menu is already
|
|
148
|
+
* click-stopped in all three renderers.
|
|
149
|
+
*/
|
|
150
|
+
{
|
|
151
|
+
label: row.isActive ? "Deactivate" : "Activate",
|
|
152
|
+
separator: true,
|
|
153
|
+
disabled: activeFlag.pendingId === row.id,
|
|
154
|
+
onClick: () => { void activeFlag.set(row.id, !row.isActive); },
|
|
155
|
+
},
|
|
124
156
|
...(!row.isDefault
|
|
125
157
|
? [{ label: "Set as Default", disabled: settingDefaultId === row.id, onClick: () => handleSetDefault(row.id) }]
|
|
126
158
|
: []),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { Alert, Badge, Checkbox, Div, FormField, FormGroup, Heading, Label, Row, Span, Stack, Text, Toggle } from "../../../ui/index.js";
|
|
@@ -8,6 +8,7 @@ import { FormShellContext, useFormShellState, applyZodIssues, FormErrorSummary }
|
|
|
8
8
|
import { SectionForm, useSectionFormNav, annotate } from "../../shell/index.js";
|
|
9
9
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
10
10
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
11
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
11
12
|
const __P = {
|
|
12
13
|
p3: "p-[var(--appkit-space-3)]",
|
|
13
14
|
};
|
|
@@ -153,6 +154,20 @@ const DEFAULT_DRAFT = {
|
|
|
153
154
|
*/
|
|
154
155
|
advancedListingTypesEnabled: false,
|
|
155
156
|
};
|
|
157
|
+
/**
|
|
158
|
+
* One radio group for a single-value field, with ONE error message.
|
|
159
|
+
*
|
|
160
|
+
* Extracted because the payout-method and account-type groups were the same
|
|
161
|
+
* eleven lines twice, and because adding the error handling to both tipped
|
|
162
|
+
* this view past `audit-code-quality`'s component-size threshold. The error
|
|
163
|
+
* placement is the part worth sharing: `aria-invalid` goes on every radio so
|
|
164
|
+
* assistive tech marks the whole group, but the message renders ONCE
|
|
165
|
+
* underneath — the field is one value, so an `error` per radio would print
|
|
166
|
+
* and announce the same sentence for every option.
|
|
167
|
+
*/
|
|
168
|
+
function RadioFieldGroup({ name, value, options, error, disabled, onSelect, renderOption, layout: Layout, }) {
|
|
169
|
+
return (_jsxs(_Fragment, { children: [_jsx(Layout, { gap: "sm", children: options.map((option) => (_jsxs(Label, { layout: "flex", gap: "md", className: "cursor-pointer", children: [_jsx(Checkbox, { bare: true, type: "radio", name: name, value: option, checked: value === option, onChange: () => onSelect(option), className: "accent-[var(--appkit-color-primary)]", disabled: disabled, "aria-invalid": error ? "true" : undefined }), renderOption(option)] }, option))) }), error && (_jsx(Text, { size: "sm", variant: "error", role: "alert", children: error }))] }));
|
|
170
|
+
}
|
|
156
171
|
export function SellerPayoutSettingsView({ apiBase = SELLER_ENDPOINTS.PAYOUT_SETTINGS }) {
|
|
157
172
|
const [draft, setDraft] = useState(DEFAULT_DRAFT);
|
|
158
173
|
const [current, setCurrent] = useState(null);
|
|
@@ -253,7 +268,22 @@ export function SellerPayoutSettingsView({ apiBase = SELLER_ENDPOINTS.PAYOUT_SET
|
|
|
253
268
|
if (Array.isArray(json?.issues) && json.issues.length > 0) {
|
|
254
269
|
applyZodIssues(json.issues, setFieldError);
|
|
255
270
|
}
|
|
256
|
-
|
|
271
|
+
/*
|
|
272
|
+
* `json.error` is the SERVER's sentence — never shown to a user
|
|
273
|
+
* (Rule #9.6). Resolve the stable `code` instead; `toUserMessage`
|
|
274
|
+
* terminates in this fallback and can never return server text.
|
|
275
|
+
*
|
|
276
|
+
* Only a banner when the issues above attached nothing: if they did,
|
|
277
|
+
* the fields now carry the detail and a banner repeating a vaguer
|
|
278
|
+
* version of it is noise.
|
|
279
|
+
*/
|
|
280
|
+
const attached = Array.isArray(json?.issues) &&
|
|
281
|
+
json.issues.some((i) => i?.path?.length);
|
|
282
|
+
if (!attached) {
|
|
283
|
+
setError(toUserMessage(json?.code, undefined, {
|
|
284
|
+
fallback: "We couldn't save your payout settings.",
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
257
287
|
return;
|
|
258
288
|
}
|
|
259
289
|
clearErrors();
|
|
@@ -263,8 +293,17 @@ export function SellerPayoutSettingsView({ apiBase = SELLER_ENDPOINTS.PAYOUT_SET
|
|
|
263
293
|
setDraft((prev) => ({ ...prev, accountNumber: "" }));
|
|
264
294
|
}
|
|
265
295
|
catch (err) {
|
|
266
|
-
|
|
267
|
-
|
|
296
|
+
/*
|
|
297
|
+
* `normalizeError` was already being called and its result DISCARDED,
|
|
298
|
+
* then the raw `.message` shown anyway — a thrown value's message is
|
|
299
|
+
* written for a developer, and this is the path a network failure or a
|
|
300
|
+
* bundler fault takes (Root Cause #86 rendered a require stack this
|
|
301
|
+
* way). Use the normalized code; the sentence is ours.
|
|
302
|
+
*/
|
|
303
|
+
const normalized = normalizeError(err);
|
|
304
|
+
setError(toUserMessage(normalized.code, undefined, {
|
|
305
|
+
fallback: "We couldn't save your payout settings. Please try again.",
|
|
306
|
+
}));
|
|
268
307
|
}
|
|
269
308
|
finally {
|
|
270
309
|
setSaving(false);
|
|
@@ -283,12 +322,12 @@ export function SellerPayoutSettingsView({ apiBase = SELLER_ENDPOINTS.PAYOUT_SET
|
|
|
283
322
|
required: true,
|
|
284
323
|
quick: true,
|
|
285
324
|
fields: ["method", "upiId", "accountHolderName", "accountNumber", "ifscCode", "bankName", "accountType"],
|
|
286
|
-
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Payout Method" }), current && (_jsx(Row, { className: "mb-2", align: "center", gap: "sm", children: _jsx(Badge, { variant: current.isConfigured ? "success" : "warning", children: current.isConfigured ? "Payout configured" : "Not configured" }) })), current?.isConfigured && (_jsx(Alert, { variant: "info", children: current.method === "upi"
|
|
325
|
+
render: ({ values, onChange, errors }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Payout Method" }), current && (_jsx(Row, { className: "mb-2", align: "center", gap: "sm", children: _jsx(Badge, { variant: current.isConfigured ? "success" : "warning", children: current.isConfigured ? "Payout configured" : "Not configured" }) })), current?.isConfigured && (_jsx(Alert, { variant: "info", children: current.method === "upi"
|
|
287
326
|
? `Current UPI: ${current.upiId}`
|
|
288
|
-
: `Current bank: ${current.bankAccount?.bankName ?? ""} — ••••${current.bankAccount?.accountNumberMasked?.slice(-4) ?? "••••"} (${current.bankAccount?.accountType})` })),
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
327
|
+
: `Current bank: ${current.bankAccount?.bankName ?? ""} — ••••${current.bankAccount?.accountNumberMasked?.slice(-4) ?? "••••"} (${current.bankAccount?.accountType})` })), _jsxs(Stack, { gap: "sm", children: [([
|
|
328
|
+
{ value: "upi", label: "UPI", desc: "Instant payouts via UPI VPA (e.g. name@upi)." },
|
|
329
|
+
{ value: "bank_transfer", label: "Bank Transfer", desc: "NEFT/RTGS to your bank account within 2–3 business days." },
|
|
330
|
+
]).map(({ value, label, desc }) => (_jsxs("label", { className: "flex items-center gap-[var(--appkit-space-3)] p-[var(--appkit-space-3)] rounded-lg border border-[var(--appkit-color-border)] cursor-pointer has-[:checked]:border-[var(--appkit-color-primary)] has-[:checked]:bg-[var(--appkit-color-primary)]/5", children: [_jsx(Checkbox, { bare: true, type: "radio", name: "payoutMethod", value: value, checked: values.method === value, onChange: () => onChange({ method: value }), className: "accent-[var(--appkit-color-primary)]", disabled: busy, "aria-invalid": errors.method ? "true" : undefined }), _jsxs(Div, { children: [_jsx(Text, { weight: "medium", children: label }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: desc })] })] }, value))), errors.method && (_jsx(Text, { size: "sm", variant: "error", role: "alert", children: errors.method }))] }), values.method === "upi" && (_jsx(FormField, { name: "upiId", label: "UPI ID (VPA)", type: "text", value: values.upiId, onChange: (v) => onChange({ upiId: v }), placeholder: "yourname@upi", helpText: "Ensure this VPA is registered and active.", disabled: busy })), values.method === "bank_transfer" && (_jsxs(Stack, { gap: "md", className: "mt-2", children: [_jsx(FormField, { name: "accountHolderName", label: "Account Holder Name", type: "text", value: values.accountHolderName, onChange: (v) => onChange({ accountHolderName: v }), placeholder: "Name as on bank account", disabled: busy }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "accountNumber", label: "Account Number", type: "text", value: values.accountNumber, onChange: (v) => onChange({ accountNumber: v }), placeholder: "Enter full account number", helpText: current?.bankAccount ? `Saved: ••••${current.bankAccount.accountNumberMasked?.slice(-4) ?? "••••"}` : "Stored securely, never displayed in full.", disabled: busy }), _jsx(FormField, { name: "ifscCode", label: "IFSC Code", type: "text", value: values.ifscCode, onChange: (v) => onChange({ ifscCode: v.toUpperCase() }), placeholder: "e.g. SBIN0001234", disabled: busy })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "bankName", label: "Bank Name", type: "text", value: values.bankName, onChange: (v) => onChange({ bankName: v }), placeholder: "e.g. State Bank of India", disabled: busy }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-1.5", size: "sm", weight: "medium", children: "Account Type" }), _jsx(RadioFieldGroup, { name: "accountType", value: values.accountType, options: ["savings", "current"], error: errors.accountType, disabled: busy, onSelect: (t) => onChange({ accountType: t }), layout: Row, renderOption: (t) => (_jsx(Text, { size: "sm", transform: "capitalize", children: t })) })] })] })] }))] })),
|
|
292
331
|
},
|
|
293
332
|
{
|
|
294
333
|
id: "tax",
|
|
@@ -641,7 +641,11 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
|
|
|
641
641
|
: `/products/${row.id}`,
|
|
642
642
|
renderRowActions: (row) => {
|
|
643
643
|
const isPublished = (statusOverrides.get(row.id) ?? row.status) === "published";
|
|
644
|
-
return (_jsxs(Row, { gap: "xs", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); handleEdit(row); }, "aria-label": ACTIONS.STORE["edit-listing"].ariaLabel, children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void openAssembly(row, "assemble"); }, "aria-label": ACTIONS.STORE["assemble-from-parts"].ariaLabel, title: ACTIONS.STORE["assemble-from-parts"].label, disabled: assembleLoading, children: _jsx(Wrench, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void openAssembly(row, "part-out"); }, "aria-label": ACTIONS.STORE["part-out-listing"].ariaLabel, title: ACTIONS.STORE["part-out-listing"].label, disabled: assembleLoading, children: _jsx(Scissors, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void handleTogglePublish(row); }, "aria-label": isPublished
|
|
644
|
+
return (_jsxs(Row, { gap: "xs", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); handleEdit(row); }, "aria-label": ACTIONS.STORE["edit-listing"].ariaLabel, children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void openAssembly(row, "assemble"); }, "aria-label": ACTIONS.STORE["assemble-from-parts"].ariaLabel, title: ACTIONS.STORE["assemble-from-parts"].label, disabled: assembleLoading, children: _jsx(Wrench, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void openAssembly(row, "part-out"); }, "aria-label": ACTIONS.STORE["part-out-listing"].ariaLabel, title: ACTIONS.STORE["part-out-listing"].label, disabled: assembleLoading, children: _jsx(Scissors, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void handleTogglePublish(row); }, "aria-label": isPublished
|
|
645
|
+
? ACTIONS.STORE["unpublish-listing"].label
|
|
646
|
+
: ACTIONS.STORE["publish-listing"].label, title: isPublished
|
|
647
|
+
? ACTIONS.STORE["unpublish-listing"].description
|
|
648
|
+
: ACTIONS.STORE["publish-listing"].description, disabled: publishingId === row.id, children: isPublished ? _jsx(EyeOff, { className: "w-4 h-4" }) : _jsx(Eye, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void handleDuplicate(row); }, "aria-label": "Duplicate listing", title: "Duplicate", children: "\u29C9" }), onDeleteProduct && (_jsx(Button, { variant: "ghost", size: "sm", action: ACTIONS.STORE["delete-listing"], onClick: (e) => { e.stopPropagation(); void handleDelete(row); }, disabled: deletingId === row.id, className: "text-[var(--appkit-color-error)] hover:bg-[var(--appkit-color-border-subtle)]", children: _jsx(Trash2, { className: "w-4 h-4" }) }))] }));
|
|
645
649
|
},
|
|
646
650
|
};
|
|
647
651
|
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), setLocationOpen && (_jsx(PhysicalLocationModal, { count: selectedIdsForLocation.length, onSave: (loc) => handleSetLocation(loc, selectedIdsForLocation), onClose: () => setSetLocationOpen(false) })), setStockOpen && (_jsx(BulkStockModal, { count: selectedIdsForStock.length, busy: stockBusy, onSave: (qty) => handleSetStock(qty, selectedIdsForStock), onClose: () => setSetStockOpen(false) })), assembleFor && assembleData && (_jsx(AssemblyDrawer, { isOpen: true,
|
|
@@ -10,6 +10,8 @@ import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
|
10
10
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
11
11
|
import { ROUTES } from "../../../constants/index.js";
|
|
12
12
|
import { toRecordArray, toStringValue } from "../hooks/useSellerListingData.js";
|
|
13
|
+
import { useOptimisticRowFlag } from "../../admin/hooks/useOptimisticRowFlag.js";
|
|
14
|
+
import { apiClient } from "../../../http/index.js";
|
|
13
15
|
import { DataListingView } from "../../admin/components/DataListingView.js";
|
|
14
16
|
const DEFAULT_SORT = "-createdAt";
|
|
15
17
|
const SORT_OPTIONS = [
|
|
@@ -40,6 +42,22 @@ const COLUMNS = [
|
|
|
40
42
|
},
|
|
41
43
|
];
|
|
42
44
|
export function SellerShippingConfigsView({ onCreateClick, onEditClick, onDelete, onSetDefault, }) {
|
|
45
|
+
/*
|
|
46
|
+
* 🛑 `isActive` was a READ-ONLY badge here, and the update schema was built
|
|
47
|
+
* to take it on its own — shipping-config's even says so in a comment: "a
|
|
48
|
+
* PATCH may legitimately carry only `isActive`". Nothing in the client had
|
|
49
|
+
* ever sent one, so flipping one boolean meant a full-editor round trip.
|
|
50
|
+
*
|
|
51
|
+
* The optimistic/rollback logic lives in the hook because eight listings
|
|
52
|
+
* grew this control at once; see its header for why rollback must DELETE
|
|
53
|
+
* the override key rather than write back a captured value.
|
|
54
|
+
*/
|
|
55
|
+
const activeFlag = useOptimisticRowFlag({
|
|
56
|
+
mutate: (id, next) => apiClient.patch(SELLER_ENDPOINTS.SHIPPING_CONFIG_BY_ID(id), { isActive: next }),
|
|
57
|
+
invalidate: [["seller", "shipping-configs", "listing"]],
|
|
58
|
+
successMessage: (next) => (next ? "Shipping option activated." : "Shipping option deactivated."),
|
|
59
|
+
errorMessage: "Could not change that shipping option's status.",
|
|
60
|
+
});
|
|
43
61
|
const [deleteTargetId, setDeleteTargetId] = useState(null);
|
|
44
62
|
const { deletingId, handleDelete: performDelete } = useEntityDelete({
|
|
45
63
|
endpoint: SELLER_ENDPOINTS.SHIPPING_CONFIG_BY_ID,
|
|
@@ -113,12 +131,26 @@ export function SellerShippingConfigsView({ onCreateClick, onEditClick, onDelete
|
|
|
113
131
|
method: String(item.method ?? ""),
|
|
114
132
|
estimatedDays: typeof item.estimatedDays === "number" ? item.estimatedDays : null,
|
|
115
133
|
isDefault: Boolean(item.isDefault),
|
|
116
|
-
|
|
134
|
+
// Resolved through the pending override so the Status badge and the
|
|
135
|
+
// row toggle cannot disagree between the click and the refetch.
|
|
136
|
+
isActive: activeFlag.resolve(toStringValue(item.id, `sc-${index}`), Boolean(item.isActive)),
|
|
117
137
|
})),
|
|
118
138
|
getTotal: (response, mappedRows) => (typeof response.total === "number" ? response.total : mappedRows.length),
|
|
119
139
|
buildFilters: () => undefined,
|
|
120
140
|
primaryAction: { label: "New Config", onClick: handleCreate },
|
|
121
141
|
renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
|
|
142
|
+
/*
|
|
143
|
+
* 🛑 `isActive` was a read-only badge and the update schema was
|
|
144
|
+
* built to accept it alone. A menu entry rather than a switch here,
|
|
145
|
+
* to match the other actions in this menu — the menu is already
|
|
146
|
+
* click-stopped in all three renderers.
|
|
147
|
+
*/
|
|
148
|
+
{
|
|
149
|
+
label: row.isActive ? "Deactivate" : "Activate",
|
|
150
|
+
separator: true,
|
|
151
|
+
disabled: activeFlag.pendingId === row.id,
|
|
152
|
+
onClick: () => { void activeFlag.set(row.id, !row.isActive); },
|
|
153
|
+
},
|
|
122
154
|
{ label: ACTIONS.STORE["edit-listing"].label, onClick: () => handleEdit(row.id) },
|
|
123
155
|
...(!row.isDefault
|
|
124
156
|
? [{ label: "Set as Default", disabled: settingDefaultId === row.id, onClick: () => handleSetDefault(row.id) }]
|
|
@@ -9,6 +9,7 @@ import { StoreAddressSelectorCreate } from "../../stores/components/StoreAddress
|
|
|
9
9
|
import { SectionForm, useSectionFormNav } from "../../shell/index.js";
|
|
10
10
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
11
11
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
12
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
12
13
|
const shippingDraftSchema = z.object({
|
|
13
14
|
customCarrierName: z.string().min(1, "Carrier name is required"),
|
|
14
15
|
customShippingPrice: z.string(),
|
|
@@ -82,7 +83,7 @@ export function SellerShippingView({ apiBase = SELLER_ENDPOINTS.SHIPPING }) {
|
|
|
82
83
|
if (Array.isArray(json?.issues) && json.issues.length > 0) {
|
|
83
84
|
applyZodIssues(json.issues, setFieldError);
|
|
84
85
|
}
|
|
85
|
-
setError(json?.
|
|
86
|
+
setError(toUserMessage(json?.code, undefined, { fallback: "We couldn't save your shipping settings." }));
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
88
89
|
clearErrors();
|
|
@@ -11,6 +11,8 @@ import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
|
11
11
|
import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers.js";
|
|
12
12
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
13
13
|
import { toRecordArray, toStringValue } from "../hooks/useSellerListingData.js";
|
|
14
|
+
import { useOptimisticRowFlag } from "../../admin/hooks/useOptimisticRowFlag.js";
|
|
15
|
+
import { apiClient } from "../../../http/index.js";
|
|
14
16
|
import { DataListingView } from "../../admin/components/DataListingView.js";
|
|
15
17
|
const DEFAULT_SORT = "displayOrder";
|
|
16
18
|
const SORT_OPTIONS = [
|
|
@@ -37,6 +39,22 @@ const COLUMNS = [
|
|
|
37
39
|
},
|
|
38
40
|
];
|
|
39
41
|
export function SellerStoreCategoriesView({ onCreateClick, onEditClick, onDelete, onBulkDelete, onBulkActivate, onBulkDeactivate, }) {
|
|
42
|
+
/*
|
|
43
|
+
* 🛑 `isActive` was a READ-ONLY badge here, and the update schema was built
|
|
44
|
+
* to take it on its own — shipping-config's even says so in a comment: "a
|
|
45
|
+
* PATCH may legitimately carry only `isActive`". Nothing in the client had
|
|
46
|
+
* ever sent one, so flipping one boolean meant a full-editor round trip.
|
|
47
|
+
*
|
|
48
|
+
* The optimistic/rollback logic lives in the hook because eight listings
|
|
49
|
+
* grew this control at once; see its header for why rollback must DELETE
|
|
50
|
+
* the override key rather than write back a captured value.
|
|
51
|
+
*/
|
|
52
|
+
const activeFlag = useOptimisticRowFlag({
|
|
53
|
+
mutate: (id, next) => apiClient.patch(SELLER_ENDPOINTS.STORE_CATEGORY_BY_ID(id), { isActive: next }),
|
|
54
|
+
invalidate: [["seller", "store-categories", "listing"]],
|
|
55
|
+
successMessage: (next) => (next ? "Category activated." : "Category deactivated."),
|
|
56
|
+
errorMessage: "Could not change that category's status.",
|
|
57
|
+
});
|
|
40
58
|
const { showToast } = useToast();
|
|
41
59
|
const [deleteTargetId, setDeleteTargetId] = useState(null);
|
|
42
60
|
const { deletingId, handleDelete: performDelete } = useEntityDelete({
|
|
@@ -79,7 +97,9 @@ export function SellerStoreCategoriesView({ onCreateClick, onEditClick, onDelete
|
|
|
79
97
|
label: String(item.label ?? ""),
|
|
80
98
|
slug: String(item.slug ?? ""),
|
|
81
99
|
productCount: Number(item.productIds?.length ?? 0),
|
|
82
|
-
|
|
100
|
+
// Resolved through the pending override so the Status badge and the
|
|
101
|
+
// row toggle cannot disagree between the click and the refetch.
|
|
102
|
+
isActive: activeFlag.resolve(toStringValue(item.id, `cat-${index}`), Boolean(item.isActive)),
|
|
83
103
|
})),
|
|
84
104
|
getTotal: (response, rows) => (typeof response.total === "number" ? response.total : rows.length),
|
|
85
105
|
buildFilters: () => undefined,
|
|
@@ -93,6 +113,18 @@ export function SellerStoreCategoriesView({ onCreateClick, onEditClick, onDelete
|
|
|
93
113
|
return actions;
|
|
94
114
|
},
|
|
95
115
|
renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
|
|
116
|
+
/*
|
|
117
|
+
* 🛑 `isActive` was a read-only badge and the update schema was
|
|
118
|
+
* built to accept it alone. A menu entry rather than a switch here,
|
|
119
|
+
* to match the other actions in this menu — the menu is already
|
|
120
|
+
* click-stopped in all three renderers.
|
|
121
|
+
*/
|
|
122
|
+
{
|
|
123
|
+
label: row.isActive ? "Deactivate" : "Activate",
|
|
124
|
+
separator: true,
|
|
125
|
+
disabled: activeFlag.pendingId === row.id,
|
|
126
|
+
onClick: () => { void activeFlag.set(row.id, !row.isActive); },
|
|
127
|
+
},
|
|
96
128
|
{
|
|
97
129
|
label: ACTIONS.STORE["edit-listing"].label,
|
|
98
130
|
onClick: () => onEditClick
|
|
@@ -24,7 +24,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
24
24
|
* close a complaint made against them.
|
|
25
25
|
*/
|
|
26
26
|
import React from "react";
|
|
27
|
-
import {
|
|
27
|
+
import { useQuery } from "@tanstack/react-query";
|
|
28
|
+
import { Badge, RecordDetailModal, Span, Stack, Text } from "../../../ui/index.js";
|
|
29
|
+
import { apiClient } from "../../../http/index.js";
|
|
28
30
|
import { DataListingView } from "../../admin/components/DataListingView.js";
|
|
29
31
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
30
32
|
import { SUPPORT_TICKET_FIELDS } from "../../../constants/field-names.js";
|
|
@@ -38,7 +40,58 @@ const STATUS_VARIANT = {
|
|
|
38
40
|
resolved: "success",
|
|
39
41
|
closed: "default",
|
|
40
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* One ticket, read-only, in the shared detail modal.
|
|
45
|
+
*
|
|
46
|
+
* Its own component so the query only runs once a row has actually been
|
|
47
|
+
* opened — the list is the common case and nobody should pay a per-ticket
|
|
48
|
+
* fetch to scroll it.
|
|
49
|
+
*/
|
|
50
|
+
function SellerSupportTicketModal({ ticketId, onClose, }) {
|
|
51
|
+
const { data, isLoading } = useQuery({
|
|
52
|
+
queryKey: ["store", "support-ticket", ticketId],
|
|
53
|
+
queryFn: () => apiClient.get(SELLER_ENDPOINTS.SUPPORT_TICKET_BY_ID(ticketId)),
|
|
54
|
+
staleTime: 30000,
|
|
55
|
+
});
|
|
56
|
+
const ticket = data?.ticket;
|
|
57
|
+
const messages = Array.isArray(ticket?.messages)
|
|
58
|
+
? ticket.messages
|
|
59
|
+
: [];
|
|
60
|
+
return (_jsx(RecordDetailModal, { isOpen: true, onClose: onClose, title: toStringValue(ticket?.subject, isLoading ? "Loading…" : "Ticket"), badges: ticket
|
|
61
|
+
? [
|
|
62
|
+
{
|
|
63
|
+
label: toStringValue(ticket.status).replace(/_/g, " "),
|
|
64
|
+
variant: STATUS_VARIANT[toStringValue(ticket.status)] ?? "default",
|
|
65
|
+
},
|
|
66
|
+
{ label: toStringValue(ticket.category).replace(/_/g, " "), variant: "default" },
|
|
67
|
+
]
|
|
68
|
+
: undefined, description: toStringValue(ticket?.description), fields: ticket
|
|
69
|
+
? [
|
|
70
|
+
{ label: "Ticket", value: toStringValue(ticket.id) },
|
|
71
|
+
{ label: "Raised", value: toRelativeDate(ticket.createdAt) },
|
|
72
|
+
{ label: "Last update", value: toRelativeDate(ticket.updatedAt ?? ticket.createdAt) },
|
|
73
|
+
{ label: "Priority", value: toStringValue(ticket.priority, "—") },
|
|
74
|
+
]
|
|
75
|
+
: undefined, items: messages.length > 0
|
|
76
|
+
? {
|
|
77
|
+
heading: `Thread (${messages.length})`,
|
|
78
|
+
entries: messages.map((m) => ({
|
|
79
|
+
/*
|
|
80
|
+
* The author's ROLE, not their name. A seller reading a
|
|
81
|
+
* complaint made against them does not need the buyer's
|
|
82
|
+
* identity to follow the thread, and those fields are
|
|
83
|
+
* PII-encrypted at rest precisely so they are not handed out
|
|
84
|
+
* by default.
|
|
85
|
+
*/
|
|
86
|
+
title: m.authorRole === "admin" ? "Support team" : "Buyer",
|
|
87
|
+
subtitle: toStringValue(m.body),
|
|
88
|
+
trailing: toRelativeDate((m.createdAt ?? null)),
|
|
89
|
+
})),
|
|
90
|
+
}
|
|
91
|
+
: undefined }));
|
|
92
|
+
}
|
|
41
93
|
export function SellerSupportView({ labels = {} }) {
|
|
94
|
+
const [detailId, setDetailId] = React.useState(null);
|
|
42
95
|
const mapRows = React.useCallback((response) => {
|
|
43
96
|
const items = Array.isArray(response?.items) ? response.items : [];
|
|
44
97
|
return items.map((raw) => {
|
|
@@ -96,5 +149,19 @@ export function SellerSupportView({ labels = {} }) {
|
|
|
96
149
|
},
|
|
97
150
|
{ key: "updated", header: "Updated" },
|
|
98
151
|
],
|
|
99
|
-
|
|
152
|
+
/*
|
|
153
|
+
* 🛑 This list was a DEAD END, and unlike most of the ones fixed
|
|
154
|
+
* alongside it that was not a pure UI gap: a seller could read a
|
|
155
|
+
* subject and a 140-character slice of the description and never
|
|
156
|
+
* open the thread, under the footer below promising the thread is
|
|
157
|
+
* where replies happen. `/api/store/support` was GET-list-only with
|
|
158
|
+
* no `[id]` sibling at all, so the route had to be built first.
|
|
159
|
+
*
|
|
160
|
+
* A modal rather than a page: the seller cannot act on these — a
|
|
161
|
+
* ticket raised ABOUT a store is not one its owner can reply to or
|
|
162
|
+
* close — so there is nothing here a route would add, and this is
|
|
163
|
+
* the same `RecordDetailModal` the other dead ends were fixed with.
|
|
164
|
+
*/
|
|
165
|
+
onRowClick: (row) => setDetailId(row.id),
|
|
166
|
+
} }), detailId && (_jsx(SellerSupportTicketModal, { ticketId: detailId, onClose: () => setDetailId(null) })), _jsx(Text, { size: "xs", color: "muted", children: "Replies and status changes happen in the ticket thread with our support team \u2014 a ticket raised about your store is not one you can close." })] }));
|
|
100
167
|
}
|
|
@@ -199,6 +199,7 @@ export declare const adminCouponFormSchema: z.ZodEffects<z.ZodObject<{
|
|
|
199
199
|
description: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
200
200
|
buyQuantity: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
201
201
|
getQuantity: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
202
|
+
maxItemPrice: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
202
203
|
currentUsage: z.ZodOptional<z.ZodNumber>;
|
|
203
204
|
tiers: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
204
205
|
minAmount: z.ZodNumber;
|
|
@@ -233,6 +234,7 @@ export declare const adminCouponFormSchema: z.ZodEffects<z.ZodObject<{
|
|
|
233
234
|
discountValue: number;
|
|
234
235
|
}[] | undefined;
|
|
235
236
|
currentUsage?: number | undefined;
|
|
237
|
+
maxItemPrice?: string | undefined;
|
|
236
238
|
maxDiscount?: string | undefined;
|
|
237
239
|
buyQuantity?: string | undefined;
|
|
238
240
|
getQuantity?: string | undefined;
|
|
@@ -257,6 +259,7 @@ export declare const adminCouponFormSchema: z.ZodEffects<z.ZodObject<{
|
|
|
257
259
|
discountValue: number;
|
|
258
260
|
}[] | undefined;
|
|
259
261
|
currentUsage?: number | undefined;
|
|
262
|
+
maxItemPrice?: string | undefined;
|
|
260
263
|
maxDiscount?: string | undefined;
|
|
261
264
|
buyQuantity?: string | undefined;
|
|
262
265
|
getQuantity?: string | undefined;
|
|
@@ -281,6 +284,7 @@ export declare const adminCouponFormSchema: z.ZodEffects<z.ZodObject<{
|
|
|
281
284
|
discountValue: number;
|
|
282
285
|
}[] | undefined;
|
|
283
286
|
currentUsage?: number | undefined;
|
|
287
|
+
maxItemPrice?: string | undefined;
|
|
284
288
|
maxDiscount?: string | undefined;
|
|
285
289
|
buyQuantity?: string | undefined;
|
|
286
290
|
getQuantity?: string | undefined;
|
|
@@ -305,6 +309,7 @@ export declare const adminCouponFormSchema: z.ZodEffects<z.ZodObject<{
|
|
|
305
309
|
discountValue: number;
|
|
306
310
|
}[] | undefined;
|
|
307
311
|
currentUsage?: number | undefined;
|
|
312
|
+
maxItemPrice?: string | undefined;
|
|
308
313
|
maxDiscount?: string | undefined;
|
|
309
314
|
buyQuantity?: string | undefined;
|
|
310
315
|
getQuantity?: string | undefined;
|
|
@@ -236,6 +236,24 @@ export const adminCouponFormSchema = couponFormBase
|
|
|
236
236
|
section: "basics", order: 7, row: "pair", kind: "number",
|
|
237
237
|
when: (v) => v.type === "buy_x_get_y",
|
|
238
238
|
}),
|
|
239
|
+
/*
|
|
240
|
+
* The per-ITEM price ceiling — "buy 3 get 1 free on items under ₹2,000".
|
|
241
|
+
*
|
|
242
|
+
* 🛑 Genuinely optional, unlike the buy/get pair: `superRefine` below does
|
|
243
|
+
* NOT demand it when the type is buy_x_get_y, because a BOGO with no
|
|
244
|
+
* ceiling is a perfectly ordinary offer and every one written before
|
|
245
|
+
* 2026-10-22 is exactly that. Leaving it blank means no ceiling.
|
|
246
|
+
*
|
|
247
|
+
* 🛑 Not to be confused with `minPurchase`, which is the ORDER total. This
|
|
248
|
+
* is one unit's price, so a line of three ₹1,500 tops qualifies on all
|
|
249
|
+
* three units rather than failing a ₹4,500 test.
|
|
250
|
+
*/
|
|
251
|
+
maxItemPrice: annotate(numericString("Maximum item price").optional(), {
|
|
252
|
+
section: "basics", order: 8, row: "pair", kind: "number",
|
|
253
|
+
when: (v) => v.type === "buy_x_get_y",
|
|
254
|
+
label: "Max item price (₹)",
|
|
255
|
+
help: "Only items at or below this price qualify. Leave blank for no limit.",
|
|
256
|
+
}),
|
|
239
257
|
/*
|
|
240
258
|
* Written by orders, never by this form. `t3-mirror` renders it read-only
|
|
241
259
|
* instead of as a box an admin could set to whatever they liked — which is
|