@mohasinac/appkit 4.61.0 → 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/jobs/core/paymentReconcile.js +87 -97
- package/dist/_internal/shared/actions/action-registry.js +149 -9
- package/dist/_internal/shared/media/limits.d.ts +10 -0
- package/dist/_internal/shared/media/limits.js +10 -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/errors/error-mapping.js +26 -1
- 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/AdminFulfillmentView.js +3 -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/ReferenceRecordEditor.js +1 -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/schemas/action-index-form.d.ts +34 -0
- package/dist/features/admin/schemas/action-index-form.js +115 -0
- package/dist/features/auth/components/RegisterForm.js +2 -1
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
- package/dist/features/events/components/AdminEventEditorView.js +8 -6
- package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
- package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
- package/dist/features/orders/components/RefundRequestView.js +4 -4
- package/dist/features/orders/schemas/index.d.ts +6 -6
- package/dist/features/products/components/GroupMemberPicker.js +4 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
- package/dist/features/products/components/ProductDetailActions.js +2 -1
- 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/shell/SectionForm.js +20 -16
- 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/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/next/routing/route-map.d.ts +4 -0
- package/dist/next/routing/route-map.js +9 -0
- package/dist/schemas/registry.d.ts +4 -4
- package/dist/schemas/sieve.d.ts +3 -3
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- 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 +43 -9
- 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
|
@@ -53,8 +53,8 @@ export declare const emiInstallmentSchema: z.ZodObject<{
|
|
|
53
53
|
}, z.ZodTypeAny, "passthrough">>]>>;
|
|
54
54
|
}, "strip", z.ZodTypeAny, {
|
|
55
55
|
status: "paid" | "pending" | "overdue";
|
|
56
|
-
amount: number;
|
|
57
56
|
index: number;
|
|
57
|
+
amount: number;
|
|
58
58
|
dueDate: string | Date | z.objectOutputType<{
|
|
59
59
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
60
60
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -68,8 +68,8 @@ export declare const emiInstallmentSchema: z.ZodObject<{
|
|
|
68
68
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
69
69
|
}, {
|
|
70
70
|
status: "paid" | "pending" | "overdue";
|
|
71
|
-
amount: number;
|
|
72
71
|
index: number;
|
|
72
|
+
amount: number;
|
|
73
73
|
dueDate: string | Date | z.objectInputType<{
|
|
74
74
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
75
75
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -405,8 +405,8 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
405
405
|
}, z.ZodTypeAny, "passthrough">>]>>;
|
|
406
406
|
}, "strip", z.ZodTypeAny, {
|
|
407
407
|
status: "paid" | "pending" | "overdue";
|
|
408
|
-
amount: number;
|
|
409
408
|
index: number;
|
|
409
|
+
amount: number;
|
|
410
410
|
dueDate: string | Date | z.objectOutputType<{
|
|
411
411
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
412
412
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -420,8 +420,8 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
420
420
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
421
421
|
}, {
|
|
422
422
|
status: "paid" | "pending" | "overdue";
|
|
423
|
-
amount: number;
|
|
424
423
|
index: number;
|
|
424
|
+
amount: number;
|
|
425
425
|
dueDate: string | Date | z.objectInputType<{
|
|
426
426
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
427
427
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -684,8 +684,8 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
684
684
|
emiSurchargeAmount?: number | undefined;
|
|
685
685
|
emiInstallments?: {
|
|
686
686
|
status: "paid" | "pending" | "overdue";
|
|
687
|
-
amount: number;
|
|
688
687
|
index: number;
|
|
688
|
+
amount: number;
|
|
689
689
|
dueDate: string | Date | z.objectOutputType<{
|
|
690
690
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
691
691
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -838,8 +838,8 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
838
838
|
emiSurchargeAmount?: number | undefined;
|
|
839
839
|
emiInstallments?: {
|
|
840
840
|
status: "paid" | "pending" | "overdue";
|
|
841
|
-
amount: number;
|
|
842
841
|
index: number;
|
|
842
|
+
amount: number;
|
|
843
843
|
dueDate: string | Date | z.objectInputType<{
|
|
844
844
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
845
845
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -29,6 +29,7 @@ import { pluginFor } from "../../../_internal/shared/listing-types/_registry.js"
|
|
|
29
29
|
import { normalizeListingType } from "../utils/listing-type.js";
|
|
30
30
|
import { resolveGroupMemberImage } from "../utils/group-member-image.js";
|
|
31
31
|
import { checkGroupRules, describeGroupRules, EMPTY_GROUP_RULES, } from "../../../_internal/shared/features/products/group-rules.js";
|
|
32
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
32
33
|
const __O = { xAuto: "overflow-x-auto", hidden: "overflow-hidden" };
|
|
33
34
|
const memberImage = resolveGroupMemberImage;
|
|
34
35
|
function memberHref(m) {
|
|
@@ -112,9 +113,11 @@ export function GroupMemberPicker({ groupId, groupSource, members, rules = EMPTY
|
|
|
112
113
|
onAuthRequired?.();
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
116
|
+
// `code` is the stable identifier the error envelope carries; `error` is
|
|
117
|
+
// the server's own sentence and is deliberately no longer read here.
|
|
115
118
|
const json = (await res.json());
|
|
116
119
|
if (!res.ok || json.success === false) {
|
|
117
|
-
setError(json.
|
|
120
|
+
setError(toUserMessage(json.code, undefined, { fallback: "Couldn't add those items to your cart." }));
|
|
118
121
|
return;
|
|
119
122
|
}
|
|
120
123
|
setQty({});
|
|
@@ -12,6 +12,7 @@ import { NonRefundableConsentModal } from "./NonRefundableConsentModal.js";
|
|
|
12
12
|
import { useAuthGate } from "../../../react/hooks/useAuthGate.js";
|
|
13
13
|
import { ACTION_ID } from "../constants/action-defs.js";
|
|
14
14
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
15
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
15
16
|
/**
|
|
16
17
|
* Client buy panel for a prize-draw detail page (SB4-G).
|
|
17
18
|
*
|
|
@@ -81,7 +82,7 @@ export function PrizeDrawEntryActions({ productId, productSlug, title, pricePerE
|
|
|
81
82
|
* a correct refusal look like the same silent nothing this flow did
|
|
82
83
|
* before it was repaired.
|
|
83
84
|
*/
|
|
84
|
-
showToast(n.
|
|
85
|
+
showToast(toUserMessage(n.code, undefined, { fallback: "We couldn't enter you into that draw. Please try again." }), "error");
|
|
85
86
|
return;
|
|
86
87
|
}
|
|
87
88
|
router.push(String(ROUTES.USER.CART));
|
|
@@ -15,6 +15,7 @@ import { useBottomActions } from "../../layout/hooks/useBottomActions.js";
|
|
|
15
15
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
16
16
|
import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices.js";
|
|
17
17
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
18
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
18
19
|
/**
|
|
19
20
|
* Wires the three primary product-detail CTAs (Buy Now / Add to Cart / Wishlist)
|
|
20
21
|
* to the server APIs with guest-fallback. Replaces the previously-unwired
|
|
@@ -35,7 +36,7 @@ export function ProductDetailActions({ productId, productTitle, productImage, pr
|
|
|
35
36
|
// itself — centralised so every add-to-cart call site gets the same
|
|
36
37
|
// richer toast, not just this one.
|
|
37
38
|
const addCart = useAddToCart({
|
|
38
|
-
onError: (err) => showToast(err?.
|
|
39
|
+
onError: (err) => showToast(toUserMessage(err?.code, undefined, { fallback: "Could not add to cart" }), "error"),
|
|
39
40
|
});
|
|
40
41
|
async function handleAddToCart(then) {
|
|
41
42
|
setBusy("cart");
|
|
@@ -9,7 +9,8 @@ import { apiClient } from "../../../http/index.js";
|
|
|
9
9
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
10
10
|
import { BarcodeField } from "./BarcodeField.js";
|
|
11
11
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
12
|
-
import { useToast } from "../../../ui/index.js";
|
|
12
|
+
import { useToast, TextLink } from "../../../ui/index.js";
|
|
13
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
13
14
|
function statusBadgeVariant(status) {
|
|
14
15
|
if (status === "confirmed")
|
|
15
16
|
return "warning";
|
|
@@ -118,5 +119,5 @@ export function FulfillmentView({ currentUserId, }) {
|
|
|
118
119
|
setScanError(null);
|
|
119
120
|
}, children: "Cancel" })] })] })), !scanResult && !scanError && (_jsx(Text, { color: "muted", size: "sm", children: orders.length === 0 && !isLoadingOrders
|
|
120
121
|
? "No orders waiting — great work!"
|
|
121
|
-
: `${orders.length} order${orders.length === 1 ? "" : "s"} in queue.` }))] })), mode === "list" && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Button, { variant: filterMine ? "primary" : "secondary", size: "sm", onClick: () => setFilterMine((v) => !v), children: filterMine ? "Showing My Orders" : "My Orders" }), _jsx(Text, { size: "xs", color: "muted", children: isLoadingOrders ? "Loading…" : `${visibleOrders.length} order${visibleOrders.length === 1 ? "" : "s"}` })] }), visibleOrders.length === 0 && !isLoadingOrders && (_jsx(Text, { color: "muted", children: "No orders waiting \u2014 great work!" })), visibleOrders.map((order) => (_jsxs(Stack, { surface: "card", padding: "sm", rounded: "md", gap: "xs", children: [_jsxs(Row, { align: "center", gap: "xs", wrap: true, children: [_jsxs(
|
|
122
|
+
: `${orders.length} order${orders.length === 1 ? "" : "s"} in queue.` }))] })), mode === "list" && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Button, { variant: filterMine ? "primary" : "secondary", size: "sm", onClick: () => setFilterMine((v) => !v), children: filterMine ? "Showing My Orders" : "My Orders" }), _jsx(Text, { size: "xs", color: "muted", children: isLoadingOrders ? "Loading…" : `${visibleOrders.length} order${visibleOrders.length === 1 ? "" : "s"}` })] }), visibleOrders.length === 0 && !isLoadingOrders && (_jsx(Text, { color: "muted", children: "No orders waiting \u2014 great work!" })), visibleOrders.map((order) => (_jsxs(Stack, { surface: "card", padding: "sm", rounded: "md", gap: "xs", children: [_jsxs(Row, { align: "center", gap: "xs", wrap: true, children: [_jsxs(TextLink, { href: String(ROUTES.STORE.ORDER_DETAIL(order.id)), size: "sm", weight: "medium", children: ["#", order.id] }), _jsx(Badge, { variant: statusBadgeVariant(order.status), children: order.status }), order.pickedAt && (_jsx(Badge, { variant: "success", children: "Picked" })), order.packedAt && (_jsx(Badge, { variant: "success", children: "Packed" }))] }), _jsxs(Text, { size: "xs", color: "muted", children: [order.userName, order.items && order.items.length > 0 && (_jsxs(_Fragment, { children: [" \u00B7 ", order.items.map((i) => i.productTitle).join(", ")] }))] }), order.assignedWorkerId && (_jsxs(Text, { size: "xs", color: "muted", children: ["Assigned: ", order.assignedWorkerId === currentUserId ? "Me" : order.assignedWorkerId] })), _jsxs(Row, { gap: "xs", children: [!order.pickedAt && (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => handleMarkPicked(order.id), isLoading: actionLoadingId === order.id + ":picked", children: "Mark Picked" })), order.pickedAt && !order.packedAt && (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => handleMarkPacked(order.id), isLoading: actionLoadingId === order.id + ":packed", children: "Mark Packed" }))] })] }, order.id)))] }))] }));
|
|
122
123
|
}
|
|
@@ -11,6 +11,7 @@ import { useEntityDelete } from "../../../react/hooks/useEntityDelete.js";
|
|
|
11
11
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
12
12
|
import { TextLink } from "../../../ui/index.js";
|
|
13
13
|
import { ROUTES } from "../../../constants/index.js";
|
|
14
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
14
15
|
const __O = {
|
|
15
16
|
xAuto: "overflow-x-auto",
|
|
16
17
|
};
|
|
@@ -47,9 +48,9 @@ function fromDoc(doc) {
|
|
|
47
48
|
isDefault: doc.isDefault,
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
|
-
function AddressCard({ address, onEdit, onDelete, }) {
|
|
51
|
+
function AddressCard({ address, onEdit, onDelete, onSetDefault, }) {
|
|
51
52
|
const isBanned = address.banStatus === "banned";
|
|
52
|
-
return (_jsxs(Stack, { surface: "card", padding: "sm", gap: "sm", children: [_jsxs(Row, { align: "start", justify: "between", gap: "xs", children: [_jsxs(Row, { gap: "xs", wrap: true, className: "min-w-0", children: [_jsx(MapPin, { className: "h-4 w-4 shrink-0 text-[var(--appkit-color-primary)]" }), _jsx(Span, { size: "sm", weight: "semibold", className: "truncate", color: "primary", children: address.label }), address.isDefault && (_jsxs(Span, { size: "xs", weight: "medium", className: CLS_DEFAULT_PILL, children: [_jsx(Star, { className: "h-3 w-3" }), "Default"] })), isBanned && _jsx(Badge, { variant: "danger", size: "sm", children: "Banned" })] }), _jsx(Row, { gap: "px", className: "shrink-0", children: isBanned ? (_jsx(Span, { size: "xs", color: "muted", title: "Contact support to resolve address ban", children: "Contact support" })) : (_jsxs(_Fragment, { children: [_jsx(IconButton, { type: "button", onClick: onEdit, title: "Edit address", "aria-label": "Edit address", variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface-elevated)] hover:text-[var(--appkit-color-text-muted)]", children: _jsx(Pencil, { className: "h-4 w-4" }) }), _jsx(IconButton, { type: "button", onClick: onDelete, title: "Delete address", "aria-label": "Delete address", variant: "ghost", rounded: "lg", className: CLS_DELETE_BTN, children: _jsx(Trash2, { className: "h-4 w-4" }) })] })) })] }), _jsxs(Text, { size: "sm", color: "muted", children: [address.fullName, " \u00B7 ", address.phone] }), _jsxs(Text, { className: "leading-relaxed", color: "muted", size: "xs", children: [address.addressLine1, address.addressLine2 ? `, ${address.addressLine2}` : "", address.landmark ? ` (near ${address.landmark})` : "", _jsx("br", {}), address.city, ", ", address.state, " ", address.postalCode, ", ", address.country] })] }));
|
|
53
|
+
return (_jsxs(Stack, { surface: "card", padding: "sm", gap: "sm", children: [_jsxs(Row, { align: "start", justify: "between", gap: "xs", children: [_jsxs(Row, { gap: "xs", wrap: true, className: "min-w-0", children: [_jsx(MapPin, { className: "h-4 w-4 shrink-0 text-[var(--appkit-color-primary)]" }), _jsx(Span, { size: "sm", weight: "semibold", className: "truncate", color: "primary", children: address.label }), address.isDefault && (_jsxs(Span, { size: "xs", weight: "medium", className: CLS_DEFAULT_PILL, children: [_jsx(Star, { className: "h-3 w-3" }), "Default"] })), isBanned && _jsx(Badge, { variant: "danger", size: "sm", children: "Banned" })] }), _jsx(Row, { gap: "px", className: "shrink-0", children: isBanned ? (_jsx(Span, { size: "xs", color: "muted", title: "Contact support to resolve address ban", children: "Contact support" })) : (_jsxs(_Fragment, { children: [!address.isDefault && (_jsx(IconButton, { type: "button", onClick: onSetDefault, title: "Set as default pickup address", "aria-label": `Set ${address.label} as the default pickup address`, variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface-elevated)] hover:text-[var(--appkit-color-text-muted)]", children: _jsx(Star, { className: "h-4 w-4" }) })), _jsx(IconButton, { type: "button", onClick: onEdit, title: "Edit address", "aria-label": "Edit address", variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface-elevated)] hover:text-[var(--appkit-color-text-muted)]", children: _jsx(Pencil, { className: "h-4 w-4" }) }), _jsx(IconButton, { type: "button", onClick: onDelete, title: "Delete address", "aria-label": "Delete address", variant: "ghost", rounded: "lg", className: CLS_DELETE_BTN, children: _jsx(Trash2, { className: "h-4 w-4" }) })] })) })] }), _jsxs(Text, { size: "sm", color: "muted", children: [address.fullName, " \u00B7 ", address.phone] }), _jsxs(Text, { className: "leading-relaxed", color: "muted", size: "xs", children: [address.addressLine1, address.addressLine2 ? `, ${address.addressLine2}` : "", address.landmark ? ` (near ${address.landmark})` : "", _jsx("br", {}), address.city, ", ", address.state, " ", address.postalCode, ", ", address.country] })] }));
|
|
53
54
|
}
|
|
54
55
|
// ---------------------------------------------------------------------------
|
|
55
56
|
// Main Component
|
|
@@ -129,7 +130,7 @@ export function SellerAddressesView({ apiBase = SELLER_ENDPOINTS.STORE_ADDRESSES
|
|
|
129
130
|
}
|
|
130
131
|
catch (err) {
|
|
131
132
|
void normalizeError(err);
|
|
132
|
-
setSaveError(err
|
|
133
|
+
setSaveError(toUserMessage(err?.code, undefined, { fallback: "Failed to save address" }));
|
|
133
134
|
}
|
|
134
135
|
finally {
|
|
135
136
|
setSaving(false);
|
|
@@ -141,9 +142,36 @@ export function SellerAddressesView({ apiBase = SELLER_ENDPOINTS.STORE_ADDRESSES
|
|
|
141
142
|
onSuccess: () => { load(); },
|
|
142
143
|
});
|
|
143
144
|
const handleDelete = (addr) => setDeleteTargetAddr(addr);
|
|
145
|
+
/**
|
|
146
|
+
* Promote one pickup address to the default.
|
|
147
|
+
*
|
|
148
|
+
* A partial PUT rather than a dedicated endpoint: unlike buyer addresses,
|
|
149
|
+
* which have `/api/user/addresses/[id]/set-default`, the store side has no
|
|
150
|
+
* such route — but `addressUpdateSchema` is the form schema `.partial()`,
|
|
151
|
+
* so `{ isDefault: true }` on its own is a valid body.
|
|
152
|
+
*/
|
|
153
|
+
const handleSetDefault = async (addr) => {
|
|
154
|
+
try {
|
|
155
|
+
// Same request shape as `handleSave` below, rather than a second
|
|
156
|
+
// hand-rolled fetch beside it.
|
|
157
|
+
const method = "PUT";
|
|
158
|
+
const res = await fetch(`${apiBase}/${addr.id}`, {
|
|
159
|
+
method,
|
|
160
|
+
headers: { "Content-Type": "application/json" },
|
|
161
|
+
body: JSON.stringify({ isDefault: true }),
|
|
162
|
+
});
|
|
163
|
+
if (!res.ok)
|
|
164
|
+
throw new Error("Failed to set default address");
|
|
165
|
+
await load();
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
const normalized = normalizeError(err);
|
|
169
|
+
setSaveError(toUserMessage(normalized.code, undefined, { fallback: "Failed to set default address" }));
|
|
170
|
+
}
|
|
171
|
+
};
|
|
144
172
|
const set = (key, value) => setDraft((p) => ({ ...p, [key]: value }));
|
|
145
173
|
const setField = (key) => (value) => set(key, value);
|
|
146
|
-
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(StickyToolbar, { offset: "header", tone: "default", border: true, padding: "md", children: _jsxs(Row, { justify: "between", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", color: "primary", children: "Pickup Addresses" }), _jsx(Text, { size: "xs", color: "muted", className: "mt-0.5", children: "Manage your store's pickup and return locations" })] }), _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(TextLink, { href: String(ROUTES.STORE.ADDRESSES_NEW), children: "Open full page \u2192" }), _jsxs(Button, { gap: "sm", size: "sm", onClick: openAdd, children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx(Span, { children: "Add Address" })] })] })] }) }), _jsxs(Div, { paddingX: "x-md-lg", className: "max-w-2xl", padding: "y-lg", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), isLoading ? (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })) : addresses.length === 0 ? (_jsxs(Stack, { border: "default", className: "border-2 border-dashed", padding: "y-4xl", align: "center", gap: "3", rounded: "xl", children: [_jsx(MapPin, { className: "h-8 w-8 text-[var(--appkit-color-text-muted)]" }), _jsx(Text, { size: "sm", color: "muted", children: "No pickup addresses yet" }), _jsx(Button, { size: "sm", variant: "outline", onClick: openAdd, children: "Add your first address" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Row, { justify: "end", className: "mb-2", children: [_jsx(Button, { size: "sm", variant: listView === "table" ? "primary" : "ghost", onClick: () => setListView("table"), children: "Table" }), _jsx(Button, { size: "sm", variant: listView === "cards" ? "primary" : "ghost", onClick: () => setListView("cards"), children: "Cards" })] }), listView === "cards" ? (_jsx(Grid, { gap: "sm", children: addresses.map((addr) => (_jsx(Div, { className: deletingId === addr.id ? "opacity-50 pointer-events-none" : "", children: _jsx(AddressCard, { address: addr, onEdit: () => openEdit(addr), onDelete: () => handleDelete(addr) }) }, addr.id))) })) : (_jsx(Div, { className: `${__O.xAuto}`, rounded: "lg", border: "default", children: _jsxs(Table, { size: "sm", children: [_jsx(Thead, { surface: "muted", children: _jsxs(Tr, { children: [_jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Label" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Name" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "City" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Phone" }), _jsx(Th, { className: "text-right", padding: "sm", weight: "semibold", children: "Actions" })] }) }), _jsx(Tbody, { children: addresses.map((addr) => {
|
|
174
|
+
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(StickyToolbar, { offset: "header", tone: "default", border: true, padding: "md", children: _jsxs(Row, { justify: "between", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", color: "primary", children: "Pickup Addresses" }), _jsx(Text, { size: "xs", color: "muted", className: "mt-0.5", children: "Manage your store's pickup and return locations" })] }), _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(TextLink, { href: String(ROUTES.STORE.ADDRESSES_NEW), children: "Open full page \u2192" }), _jsxs(Button, { gap: "sm", size: "sm", onClick: openAdd, children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx(Span, { children: "Add Address" })] })] })] }) }), _jsxs(Div, { paddingX: "x-md-lg", className: "max-w-2xl", padding: "y-lg", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), isLoading ? (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })) : addresses.length === 0 ? (_jsxs(Stack, { border: "default", className: "border-2 border-dashed", padding: "y-4xl", align: "center", gap: "3", rounded: "xl", children: [_jsx(MapPin, { className: "h-8 w-8 text-[var(--appkit-color-text-muted)]" }), _jsx(Text, { size: "sm", color: "muted", children: "No pickup addresses yet" }), _jsx(Button, { size: "sm", variant: "outline", onClick: openAdd, children: "Add your first address" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Row, { justify: "end", className: "mb-2", children: [_jsx(Button, { size: "sm", variant: listView === "table" ? "primary" : "ghost", onClick: () => setListView("table"), children: "Table" }), _jsx(Button, { size: "sm", variant: listView === "cards" ? "primary" : "ghost", onClick: () => setListView("cards"), children: "Cards" })] }), listView === "cards" ? (_jsx(Grid, { gap: "sm", children: addresses.map((addr) => (_jsx(Div, { className: deletingId === addr.id ? "opacity-50 pointer-events-none" : "", children: _jsx(AddressCard, { address: addr, onEdit: () => openEdit(addr), onDelete: () => handleDelete(addr), onSetDefault: () => void handleSetDefault(addr) }) }, addr.id))) })) : (_jsx(Div, { className: `${__O.xAuto}`, rounded: "lg", border: "default", children: _jsxs(Table, { size: "sm", children: [_jsx(Thead, { surface: "muted", children: _jsxs(Tr, { children: [_jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Label" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Name" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "City" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Phone" }), _jsx(Th, { className: "text-right", padding: "sm", weight: "semibold", children: "Actions" })] }) }), _jsx(Tbody, { children: addresses.map((addr) => {
|
|
147
175
|
const isBanned = addr.banStatus === "banned";
|
|
148
176
|
return (_jsxs(Tr, { className: `border-t border-[var(--appkit-color-border)] ${deletingId === addr.id ? "opacity-50" : ""}`, children: [_jsx(Td, { padding: "sm", children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Span, { children: addr.label }), isBanned && _jsx(Badge, { variant: "danger", size: "sm", children: "Banned" })] }) }), _jsx(Td, { padding: "sm", children: addr.fullName }), _jsxs(Td, { padding: "sm", children: [addr.city, ", ", addr.state] }), _jsx(Td, { className: "tabular-nums", padding: "sm", children: addr.phone }), _jsx(Td, { className: "text-right", padding: "sm", children: isBanned ? (_jsx(Span, { size: "xs", color: "muted", title: "Contact support to resolve address ban", children: "Contact support" })) : (_jsxs(Row, { justify: "end", gap: "xs", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => openEdit(addr), children: ROW_ACTION_META[ROW_ACTION_ID.EDIT].label }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => handleDelete(addr), children: ROW_ACTION_META[ROW_ACTION_ID.DELETE].label })] })) })] }, addr.id));
|
|
149
177
|
}) })] }) }))] }))] }), _jsx(SideDrawer, { isOpen: drawerOpen, onClose: closeDrawer, title: editingId ? "Edit Address" : "Add Address", footer:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
4
4
|
import React, { useState, useCallback } from "react";
|
|
5
|
+
import { z } from "zod";
|
|
5
6
|
import { alertMetricsForScope } from "../../store-extensions/constants/alert-metrics.js";
|
|
6
7
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
7
8
|
import { Alert, Badge, Button, Div, Heading, Row, Section, Stack, Text, Toggle, useToast } from "../../../ui/index.js";
|
|
@@ -14,6 +15,7 @@ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues.js";
|
|
|
14
15
|
import { buildSectionsFromSchema, visibleValues } from "../../shell/build-sections.js";
|
|
15
16
|
import { SectionForm, useSectionFormNav } from "../../shell/SectionForm.js";
|
|
16
17
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
18
|
+
import { QuickEditMenu } from "../../admin/components/QuickEditMenu.js";
|
|
17
19
|
const __P = {
|
|
18
20
|
p5: "p-[var(--appkit-space-5)]",
|
|
19
21
|
};
|
|
@@ -80,11 +82,55 @@ const OPERATOR_BADGE = {
|
|
|
80
82
|
// ---------------------------------------------------------------------------
|
|
81
83
|
// Sub-components
|
|
82
84
|
// ---------------------------------------------------------------------------
|
|
83
|
-
|
|
85
|
+
/**
|
|
86
|
+
* The three values a seller actually tunes once an alert is live.
|
|
87
|
+
*
|
|
88
|
+
* Deliberately not `metric` or `operator`: changing either turns the alert
|
|
89
|
+
* into a different alert while keeping its firing history, so
|
|
90
|
+
* `lastTriggeredAt`/`lastObservedValue` on the card would then describe a rule
|
|
91
|
+
* that no longer exists. Those stay a delete-and-recreate.
|
|
92
|
+
*/
|
|
93
|
+
const alertQuickEditSchema = z.object({
|
|
94
|
+
label: z.string().trim().min(1, "A name is required."),
|
|
95
|
+
threshold: z.coerce.number().min(0, "Threshold cannot be negative."),
|
|
96
|
+
windowHours: z.coerce.number().int().min(1),
|
|
97
|
+
});
|
|
98
|
+
const ALERT_QUICK_FIELDS = [
|
|
99
|
+
{ name: "label", label: "Name", type: "text", required: true },
|
|
100
|
+
{
|
|
101
|
+
name: "threshold",
|
|
102
|
+
label: "Threshold",
|
|
103
|
+
type: "number",
|
|
104
|
+
required: true,
|
|
105
|
+
helperText: "The value the metric is compared against.",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "windowHours",
|
|
109
|
+
label: "Window",
|
|
110
|
+
type: "select",
|
|
111
|
+
required: true,
|
|
112
|
+
options: WINDOW_OPTIONS,
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
function AlertCard({ alert, onToggle, onDelete, onEdit, }) {
|
|
84
116
|
const metricLabel = METRIC_OPTIONS.find((m) => m.value === alert.metric)?.label ?? alert.metric;
|
|
85
117
|
const opLabel = OPERATOR_OPTIONS.find((o) => o.value === alert.operator)?.label ?? alert.operator;
|
|
86
118
|
const windowLabel = WINDOW_OPTIONS.find((w) => w.value === String(alert.windowHours))?.label ?? `${alert.windowHours}h`;
|
|
87
|
-
return (_jsxs(Row, { surface: "card", padding: "sm", align: "start", justify: "between", gap: "md", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Row, { className: "mb-1", gap: "sm", wrap: true, children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: alert.label }), _jsx(Badge, { variant: alert.isActive ? "success" : "default", children: alert.isActive ? "Active" : "Paused" })] }), _jsxs(Text, { className: "mb-1", color: "muted", size: "xs", children: [metricLabel, " ", opLabel, " ", alert.threshold, " \u00B7 ", windowLabel] }), METRIC_DESCRIPTIONS[alert.metric] ? (_jsx(Text, { className: "mb-1", color: "muted", size: "xs", children: METRIC_DESCRIPTIONS[alert.metric] })) : null, alert.lastTriggeredAt ? (_jsxs(Text, { className: "mb-1", color: "muted", size: "xs", children: ["Last fired ", new Date(alert.lastTriggeredAt).toLocaleString(), alert.lastObservedValue != null ? ` at ${alert.lastObservedValue}` : ""] })) : null, _jsx(Row, { gap: "xs", wrap: true, children: alert.notifyChannels.map((ch) => (_jsx(Badge, { variant: OPERATOR_BADGE[alert.operator] ?? "default", children: ch }, ch))) }), alert.lastTriggeredAt && (_jsxs(Text, { className: "mt-1", color: "faint", size: "xs", children: ["Last triggered: ", new Date(alert.lastTriggeredAt).toLocaleString("en-IN")] }))] }), _jsxs(Row, { className: "shrink-0", align: "center", gap: "sm", children: [_jsx(Toggle, { checked: alert.isActive, onChange: (v) => onToggle(alert.id, v), size: "sm"
|
|
119
|
+
return (_jsxs(Row, { surface: "card", padding: "sm", align: "start", justify: "between", gap: "md", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Row, { className: "mb-1", gap: "sm", wrap: true, children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: alert.label }), _jsx(Badge, { variant: alert.isActive ? "success" : "default", children: alert.isActive ? "Active" : "Paused" })] }), _jsxs(Text, { className: "mb-1", color: "muted", size: "xs", children: [metricLabel, " ", opLabel, " ", alert.threshold, " \u00B7 ", windowLabel] }), METRIC_DESCRIPTIONS[alert.metric] ? (_jsx(Text, { className: "mb-1", color: "muted", size: "xs", children: METRIC_DESCRIPTIONS[alert.metric] })) : null, alert.lastTriggeredAt ? (_jsxs(Text, { className: "mb-1", color: "muted", size: "xs", children: ["Last fired ", new Date(alert.lastTriggeredAt).toLocaleString(), alert.lastObservedValue != null ? ` at ${alert.lastObservedValue}` : ""] })) : null, _jsx(Row, { gap: "xs", wrap: true, children: alert.notifyChannels.map((ch) => (_jsx(Badge, { variant: OPERATOR_BADGE[alert.operator] ?? "default", children: ch }, ch))) }), alert.lastTriggeredAt && (_jsxs(Text, { className: "mt-1", color: "faint", size: "xs", children: ["Last triggered: ", new Date(alert.lastTriggeredAt).toLocaleString("en-IN")] }))] }), _jsxs(Row, { className: "shrink-0", align: "center", gap: "sm", children: [_jsx(Toggle, { checked: alert.isActive, onChange: (v) => onToggle(alert.id, v), size: "sm", ariaLabel: `${alert.isActive ? "Pause" : "Resume"} ${alert.label}` }), _jsx(QuickEditMenu, { actions: [
|
|
120
|
+
{
|
|
121
|
+
label: "Edit alert",
|
|
122
|
+
formTitle: `Edit "${alert.label}"`,
|
|
123
|
+
schema: alertQuickEditSchema,
|
|
124
|
+
fields: ALERT_QUICK_FIELDS,
|
|
125
|
+
defaultValues: {
|
|
126
|
+
label: alert.label,
|
|
127
|
+
threshold: alert.threshold,
|
|
128
|
+
windowHours: String(alert.windowHours),
|
|
129
|
+
},
|
|
130
|
+
onSubmit: (values) => onEdit(alert.id, values),
|
|
131
|
+
submitLabel: "Save alert",
|
|
132
|
+
},
|
|
133
|
+
] }), _jsx(Button, { variant: "ghost", size: "sm", action: ACTIONS.STORE["delete-analytics-alert"], onClick: () => onDelete(alert.id), className: "text-error hover:text-error" })] })] }));
|
|
88
134
|
}
|
|
89
135
|
const EMPTY_DRAFT = {
|
|
90
136
|
label: "",
|
|
@@ -142,6 +188,17 @@ export function SellerAnalyticsAlertsView({ labels = {}, }) {
|
|
|
142
188
|
void queryClient.invalidateQueries({ queryKey: ["seller", "analytics-alerts"] });
|
|
143
189
|
},
|
|
144
190
|
});
|
|
191
|
+
const editMutation = useApiMutation({
|
|
192
|
+
errorMessage: "Failed to update alert",
|
|
193
|
+
mutationFn: async ({ id, values }) => {
|
|
194
|
+
const parsed = alertQuickEditSchema.parse(values);
|
|
195
|
+
return apiClient.patch(SELLER_ENDPOINTS.ANALYTICS_ALERT_BY_ID(id), parsed);
|
|
196
|
+
},
|
|
197
|
+
onSuccess: () => {
|
|
198
|
+
showToast("Alert updated", "success");
|
|
199
|
+
void queryClient.invalidateQueries({ queryKey: ["seller", "analytics-alerts"] });
|
|
200
|
+
},
|
|
201
|
+
});
|
|
145
202
|
const toggleChannel = useCallback((ch) => {
|
|
146
203
|
setDraft((d) => ({
|
|
147
204
|
...d,
|
|
@@ -192,5 +249,7 @@ export function SellerAnalyticsAlertsView({ labels = {}, }) {
|
|
|
192
249
|
* page's own form — a viewport-pinned Save would claim the
|
|
193
250
|
* bottom tier for a control the reader may have scrolled past.
|
|
194
251
|
*/
|
|
195
|
-
bottomBar: false })] })] })), isLoading && (_jsx(Stack, { gap: "sm", children: [1, 2, 3].map((i) => (_jsx(Div, { className: "h-16 animate-pulse", surface: "subtle", rounded: "lg" }, i))) })), !isLoading && alerts.length === 0 && !showForm && (_jsx(Alert, { variant: "info", children: "No alerts configured. Create an alert to get notified when a key metric crosses a threshold." })), !isLoading && alerts.length > 0 && (_jsx(Stack, { gap: "3", children: alerts.map((alert) => (_jsx(AlertCard, { alert: alert, onToggle: (id, isActive) => toggleMutation.mutate({ id, isActive }), onDelete: (id) => deleteMutation.mutate(id)
|
|
252
|
+
bottomBar: false })] })] })), isLoading && (_jsx(Stack, { gap: "sm", children: [1, 2, 3].map((i) => (_jsx(Div, { className: "h-16 animate-pulse", surface: "subtle", rounded: "lg" }, i))) })), !isLoading && alerts.length === 0 && !showForm && (_jsx(Alert, { variant: "info", children: "No alerts configured. Create an alert to get notified when a key metric crosses a threshold." })), !isLoading && alerts.length > 0 && (_jsx(Stack, { gap: "3", children: alerts.map((alert) => (_jsx(AlertCard, { alert: alert, onToggle: (id, isActive) => toggleMutation.mutate({ id, isActive }), onDelete: (id) => deleteMutation.mutate(id), onEdit: async (id, values) => {
|
|
253
|
+
await editMutation.mutateAsync({ id, values });
|
|
254
|
+
} }, alert.id))) }))] }));
|
|
196
255
|
}
|
|
@@ -1,12 +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 { SIEVE_OP, sieveFilter, sortBy } from "@mohasinac/appkit/client";
|
|
4
|
-
import { Badge, Button, FilterChipGroup, Stack, Text, TextLink } from "../../../ui/index.js";
|
|
4
|
+
import { Badge, Button, FilterChipGroup, Stack, Text, TextLink, Toggle } from "../../../ui/index.js";
|
|
5
5
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
6
6
|
import { ROW_ACTION_META, ROW_ACTION_ID } from "../../products/constants/action-defs.js";
|
|
7
7
|
import { SELLER_BULK_ACTIONS } from "../../products/constants/action-defs.js";
|
|
8
8
|
import { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "../../../_internal/shared/features/categories/bundle-copy.js";
|
|
9
9
|
import { toRecordArray, toRelativeDate, toStringValue, } from "../../admin/hooks/useAdminListingData.js";
|
|
10
|
+
import { useOptimisticRowFlag } from "../../admin/hooks/useOptimisticRowFlag.js";
|
|
11
|
+
import { apiClient } from "../../../http/index.js";
|
|
10
12
|
import { DataListingView } from "../../admin/components/DataListingView.js";
|
|
11
13
|
function formatPrice(value) {
|
|
12
14
|
if (typeof value !== "number" || value <= 0)
|
|
@@ -38,6 +40,22 @@ const COLUMNS = [
|
|
|
38
40
|
{ key: "updatedAt", header: "Updated", className: "w-28" },
|
|
39
41
|
];
|
|
40
42
|
export function SellerBundlesView({ editHrefTemplate, newHref }) {
|
|
43
|
+
/*
|
|
44
|
+
* 🛑 `isActive` was a READ-ONLY badge here, and the update schema was built
|
|
45
|
+
* to take it on its own — shipping-config's even says so in a comment: "a
|
|
46
|
+
* PATCH may legitimately carry only `isActive`". Nothing in the client had
|
|
47
|
+
* ever sent one, so flipping one boolean meant a full-editor round trip.
|
|
48
|
+
*
|
|
49
|
+
* The optimistic/rollback logic lives in the hook because eight listings
|
|
50
|
+
* grew this control at once; see its header for why rollback must DELETE
|
|
51
|
+
* the override key rather than write back a captured value.
|
|
52
|
+
*/
|
|
53
|
+
const activeFlag = useOptimisticRowFlag({
|
|
54
|
+
mutate: (id, next) => apiClient.put(SELLER_ENDPOINTS.BUNDLE_BY_ID(id), { isActive: next }),
|
|
55
|
+
invalidate: [["seller", "bundles", "listing"], ["bundles"]],
|
|
56
|
+
successMessage: (next) => (next ? "Bundle activated." : "Bundle deactivated."),
|
|
57
|
+
errorMessage: "Could not change that bundle's status.",
|
|
58
|
+
});
|
|
41
59
|
const config = {
|
|
42
60
|
portal: "seller",
|
|
43
61
|
title: "Bundles",
|
|
@@ -60,19 +78,29 @@ export function SellerBundlesView({ editHrefTemplate, newHref }) {
|
|
|
60
78
|
{ value: sortBy("createdAt", "ASC"), label: "Oldest" },
|
|
61
79
|
],
|
|
62
80
|
columns: COLUMNS,
|
|
63
|
-
mapRows: (response) => toRecordArray(response.items).map((item, index) =>
|
|
64
|
-
id
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
mapRows: (response) => toRecordArray(response.items).map((item, index) => {
|
|
82
|
+
const id = toStringValue(item.id, `bundle-${index}`);
|
|
83
|
+
/*
|
|
84
|
+
* Resolved ONCE, through the pending override, and then used for both
|
|
85
|
+
* `isActive` and the `status` LABEL below. Deriving the label from the
|
|
86
|
+
* raw value while the flag read the override is how the badge ends up
|
|
87
|
+
* saying "Inactive" beside a switch that is on.
|
|
88
|
+
*/
|
|
89
|
+
const isActive = activeFlag.resolve(id, item.isActive === true);
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
primary: toStringValue(item.name, "Untitled bundle"),
|
|
93
|
+
secondary: toStringValue(item.slug, "no-slug"),
|
|
94
|
+
price: formatPrice(item.bundlePrice),
|
|
95
|
+
members: String(Array.isArray(item.bundleProductIds) ? item.bundleProductIds.length : 0),
|
|
96
|
+
stockStatus: toStringValue(item.bundleStockStatus, "in_stock"),
|
|
97
|
+
isActive,
|
|
98
|
+
status: isActive
|
|
99
|
+
? BUNDLE_COPY.adminList.activeBadge
|
|
100
|
+
: BUNDLE_COPY.adminList.inactiveBadge,
|
|
101
|
+
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
102
|
+
};
|
|
103
|
+
}),
|
|
76
104
|
getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
|
|
77
105
|
buildFilters: (filterState) => {
|
|
78
106
|
const parts = [];
|
|
@@ -83,6 +111,17 @@ export function SellerBundlesView({ editHrefTemplate, newHref }) {
|
|
|
83
111
|
return parts.join(",") || undefined;
|
|
84
112
|
},
|
|
85
113
|
rowHrefTemplate: editHrefTemplate,
|
|
114
|
+
/*
|
|
115
|
+
* 🛑 This view had NO renderRowActions at all — the only per-row
|
|
116
|
+
* affordance was the row link to the editor, so a bundle could only be
|
|
117
|
+
* activated or deactivated by opening it.
|
|
118
|
+
*
|
|
119
|
+
* The toggle lives here rather than in a column because `rowHrefTemplate`
|
|
120
|
+
* makes the row an anchor: a normal column cell does not stop
|
|
121
|
+
* propagation, so a switch there would flip AND navigate. This slot is
|
|
122
|
+
* already click-stopped in all three renderers.
|
|
123
|
+
*/
|
|
124
|
+
renderRowActions: (row) => (_jsx(Toggle, { size: "sm", checked: row.isActive, disabled: activeFlag.pendingId === row.id, ariaLabel: `${row.isActive ? "Deactivate" : "Activate"} ${row.primary}`, onChange: (next) => { void activeFlag.set(row.id, next); } })),
|
|
86
125
|
toolbarExtra: (_jsx(Button, { asChild: true, size: "sm", variant: "primary", children: _jsx(TextLink, { href: newHref, layout: "flex", align: "center", gap: "xs", children: "+ New Bundle" }) })),
|
|
87
126
|
// Rule #7: bulk-action array sourced from the SELLER_BULK_ACTIONS preset.
|
|
88
127
|
buildBulkActions: (selection) => {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Row, sortBy } from "@mohasinac/appkit/client";
|
|
4
|
-
import { Badge, Button, Span } from "../../../ui/index.js";
|
|
4
|
+
import { Badge, Button, Span, Toggle } from "../../../ui/index.js";
|
|
5
5
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
6
6
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
7
7
|
import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers.js";
|
|
8
8
|
import { toRecordArray, toRelativeDate, toStringValue, } from "../../admin/hooks/useAdminListingData.js";
|
|
9
|
+
import { useOptimisticRowFlag } from "../../admin/hooks/useOptimisticRowFlag.js";
|
|
10
|
+
import { apiClient } from "../../../http/index.js";
|
|
9
11
|
import { DataListingView } from "../../admin/components/DataListingView.js";
|
|
10
12
|
const THEME_LABELS = {
|
|
11
13
|
related: "Related",
|
|
@@ -51,6 +53,22 @@ const COLUMNS = [
|
|
|
51
53
|
},
|
|
52
54
|
];
|
|
53
55
|
export function SellerGroupedListingsView({ onCreateClick, onEditClick, onDeleteClick, }) {
|
|
56
|
+
/*
|
|
57
|
+
* 🛑 `isActive` was a READ-ONLY badge here, and the update schema was built
|
|
58
|
+
* to take it on its own — shipping-config's even says so in a comment: "a
|
|
59
|
+
* PATCH may legitimately carry only `isActive`". Nothing in the client had
|
|
60
|
+
* ever sent one, so flipping one boolean meant a full-editor round trip.
|
|
61
|
+
*
|
|
62
|
+
* The optimistic/rollback logic lives in the hook because eight listings
|
|
63
|
+
* grew this control at once; see its header for why rollback must DELETE
|
|
64
|
+
* the override key rather than write back a captured value.
|
|
65
|
+
*/
|
|
66
|
+
const activeFlag = useOptimisticRowFlag({
|
|
67
|
+
mutate: (id, next) => apiClient.patch(SELLER_ENDPOINTS.GROUPED_LISTING_BY_ID(id), { isActive: next }),
|
|
68
|
+
invalidate: [["seller", "grouped-listings"], ["grouped-listings"]],
|
|
69
|
+
successMessage: (next) => (next ? "Group activated." : "Group deactivated."),
|
|
70
|
+
errorMessage: "Could not change that group's status.",
|
|
71
|
+
});
|
|
54
72
|
const config = {
|
|
55
73
|
portal: "seller",
|
|
56
74
|
title: "Grouped Listings",
|
|
@@ -72,7 +90,9 @@ export function SellerGroupedListingsView({ onCreateClick, onEditClick, onDelete
|
|
|
72
90
|
title: toStringValue(item.title, "Untitled group"),
|
|
73
91
|
groupTheme: toStringValue(item.groupTheme, "generic"),
|
|
74
92
|
productCount: Array.isArray(item.productIds) ? item.productIds.length : 0,
|
|
75
|
-
|
|
93
|
+
// Resolved through the pending override so the Status badge and the
|
|
94
|
+
// row toggle cannot disagree between the click and the refetch.
|
|
95
|
+
isActive: activeFlag.resolve(toStringValue(item.id, `group-${index}`), item.isActive === true),
|
|
76
96
|
visibilityStatus: toStringValue(item.visibilityStatus, "hidden"),
|
|
77
97
|
createdAt: toRelativeDate(item.createdAt),
|
|
78
98
|
})),
|
|
@@ -92,7 +112,7 @@ export function SellerGroupedListingsView({ onCreateClick, onEditClick, onDelete
|
|
|
92
112
|
: undefined,
|
|
93
113
|
// Mirrors the "Edit" row action below so table rows and cards both navigate on click.
|
|
94
114
|
onRowClick: onEditClick ? (row) => onEditClick(row.id) : undefined,
|
|
95
|
-
renderRowActions: (row) => (_jsxs(Row, { gap: "xs", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => onEditClick?.(row.id), children: ACTIONS.STORE["edit-listing"].label }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => onDeleteClick?.(row.id), children: ACTIONS.STORE["delete-listing"].label })] })),
|
|
115
|
+
renderRowActions: (row) => (_jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Toggle, { size: "sm", checked: row.isActive, disabled: activeFlag.pendingId === row.id, ariaLabel: `${row.isActive ? "Deactivate" : "Activate"} ${row.title}`, onChange: (next) => { void activeFlag.set(row.id, next); } }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => onEditClick?.(row.id), children: ACTIONS.STORE["edit-listing"].label }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => onDeleteClick?.(row.id), children: ACTIONS.STORE["delete-listing"].label })] })),
|
|
96
116
|
};
|
|
97
117
|
return _jsx(DataListingView, { config: config });
|
|
98
118
|
}
|
|
@@ -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
|
}
|