@mohasinac/appkit 4.61.0 → 4.62.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.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/checkout/actions.d.ts +80 -0
- package/dist/_internal/server/features/checkout/actions.js +542 -26
- package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
- package/dist/_internal/server/features/invoicing/allocate.js +162 -0
- package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
- package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
- package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
- package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
- package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
- package/dist/_internal/server/features/invoicing/issue.js +202 -0
- package/dist/_internal/server/features/orders/actions.d.ts +8 -1
- package/dist/_internal/server/features/orders/actions.js +81 -8
- package/dist/_internal/server/features/orders/adapters.js +33 -0
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
- package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
- package/dist/_internal/server/features/points/award.js +46 -4
- package/dist/_internal/server/features/refunds/actions.js +26 -0
- package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
- package/dist/_internal/server/features/shipping/courier-block.js +239 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +15 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +51 -0
- package/dist/_internal/server/functions/scheduled.js +38 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +9 -2
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
- package/dist/_internal/server/jobs/core/index.d.ts +2 -1
- package/dist/_internal/server/jobs/core/index.js +2 -1
- package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
- package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +62 -0
- package/dist/_internal/server/jobs/core/paymentReconcile.js +87 -97
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
- package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
- package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +10 -2
- package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
- package/dist/_internal/shared/actions/action-registry.js +177 -9
- package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
- package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
- package/dist/_internal/shared/checkout/order-math.d.ts +145 -0
- package/dist/_internal/shared/checkout/order-math.js +173 -0
- package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
- package/dist/_internal/shared/checkout/refundable-base.js +13 -1
- package/dist/_internal/shared/checkout/split.d.ts +87 -0
- package/dist/_internal/shared/checkout/split.js +100 -0
- package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
- package/dist/_internal/shared/content/disclaimers.js +217 -0
- package/dist/_internal/shared/content/published-values.d.ts +88 -0
- package/dist/_internal/shared/content/published-values.js +198 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +109 -0
- package/dist/_internal/shared/features/admin/integrations.js +253 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
- package/dist/_internal/shared/features/points/accepts.d.ts +36 -0
- package/dist/_internal/shared/features/points/accepts.js +43 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
- package/dist/_internal/shared/fees/calculator.d.ts +33 -1
- package/dist/_internal/shared/fees/calculator.js +32 -2
- 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 +12 -1
- package/dist/client.js +31 -1
- package/dist/constants/api-endpoints.d.ts +45 -0
- package/dist/constants/api-endpoints.js +15 -0
- package/dist/constants/field-names.d.ts +18 -0
- package/dist/constants/field-names.js +28 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/shipping.d.ts +45 -0
- package/dist/contracts/shipping.js +15 -0
- package/dist/core/server-action.js +19 -3
- package/dist/errors/error-mapping.js +26 -1
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
- package/dist/features/account/components/UserOffersPanel.js +5 -3
- package/dist/features/account/components/UserOrdersView.js +19 -33
- 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 +17 -0
- package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
- package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
- package/dist/features/admin/components/AdminInvoicingView.js +65 -0
- 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/AdminOrderEditorView.d.ts +18 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +11 -2
- package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
- package/dist/features/admin/components/AdminOrdersView.js +67 -5
- 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/OwnerQuickActions.js +24 -2
- package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
- package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
- package/dist/features/admin/constants/filter-tabs.js +6 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
- package/dist/features/admin/hooks/useAdminListingData.js +16 -0
- 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/admin/schemas/admin-user-form.d.ts +2 -2
- package/dist/features/admin/schemas/firestore.d.ts +30 -0
- package/dist/features/admin/schemas/firestore.js +5 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +30 -0
- package/dist/features/audit-log/schemas/firestore.js +15 -0
- package/dist/features/auth/components/RegisterForm.js +2 -1
- package/dist/features/auth/permissions/constants.js +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +6 -2
- package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
- package/dist/features/cart/components/PointsTenderPicker.js +15 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
- package/dist/features/cart/components/index.d.ts +2 -0
- package/dist/features/cart/components/index.js +1 -0
- package/dist/features/cart/repository/cart.repository.d.ts +19 -0
- package/dist/features/cart/repository/cart.repository.js +27 -0
- package/dist/features/cart/schemas/firestore.d.ts +92 -0
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
- package/dist/features/contact/email.d.ts +27 -10
- package/dist/features/contact/email.js +20 -3
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +38 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +80 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +64 -0
- package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
- package/dist/features/events/components/AdminEventEditorView.js +8 -6
- package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
- package/dist/features/faq/components/FAQAccordion.js +29 -4
- package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
- package/dist/features/invoicing/schemas/firestore.js +45 -0
- package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
- package/dist/features/invoicing/utils/financial-year.js +53 -0
- package/dist/features/jobs/actions/enqueue-job.js +18 -1
- package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
- package/dist/features/jobs/repository/jobs.repository.js +84 -1
- package/dist/features/jobs/schemas/firestore.d.ts +80 -0
- package/dist/features/jobs/schemas/firestore.js +37 -0
- package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
- package/dist/features/lottery/schemas/zod.d.ts +6 -6
- package/dist/features/media/MediaImage.js +22 -1
- package/dist/features/media/upload/ImageUpload.js +1 -1
- package/dist/features/obligations/constants/policies.js +24 -1
- package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
- package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
- package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
- package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
- package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +111 -0
- package/dist/features/orders/components/RefundRequestView.js +22 -4
- package/dist/features/orders/components/index.d.ts +4 -0
- package/dist/features/orders/components/index.js +2 -0
- package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
- package/dist/features/orders/constants/order-lane-tabs.js +88 -0
- package/dist/features/orders/repository/orders.repository.d.ts +57 -0
- package/dist/features/orders/repository/orders.repository.js +145 -0
- package/dist/features/orders/schemas/firestore.d.ts +196 -1
- package/dist/features/orders/schemas/index.d.ts +60 -12
- package/dist/features/orders/schemas/index.js +12 -0
- package/dist/features/orders/types/index.d.ts +26 -0
- package/dist/features/orders/utils/courier-block.d.ts +25 -0
- package/dist/features/orders/utils/courier-block.js +23 -0
- package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
- package/dist/features/payment-cases/schemas/firestore.js +1 -0
- package/dist/features/products/components/GroupMemberPicker.js +4 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
- package/dist/features/products/components/ProductDetailActions.js +2 -1
- package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
- package/dist/features/products/components/ProductDetailPageView.js +2 -2
- package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
- package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
- package/dist/features/promotions/actions/coupon-stacking.js +21 -2
- package/dist/features/promotions/components/CouponCard.js +13 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
- package/dist/features/promotions/constants/coupon-help.js +9 -1
- package/dist/features/promotions/schemas/firestore.d.ts +24 -0
- package/dist/features/promotions/schemas/firestore.js +4 -0
- package/dist/features/promotions/types/index.d.ts +19 -1
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/action-index/tab-entries.js +1 -1
- package/dist/features/seller/actions/seller-actions.js +33 -3
- 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/SellerOrdersView.js +129 -27
- 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/hooks/useSellerListingData.js +25 -0
- 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/shipments/schemas/validation.d.ts +4 -4
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +43 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +90 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +82 -0
- package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
- 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/store-extensions/schemas/report-create-form.d.ts +2 -2
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
- package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
- package/dist/features/stores/schemas/firestore.d.ts +71 -0
- package/dist/features/stores/types/index.d.ts +19 -0
- package/dist/features/support/schemas/index.d.ts +6 -6
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.js +17 -2
- package/dist/next/routing/route-map.d.ts +24 -0
- package/dist/next/routing/route-map.js +19 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
- package/dist/providers/shipping-shiprocket/index.js +84 -1
- package/dist/schemas/registry.d.ts +51 -22
- package/dist/schemas/sieve.d.ts +5 -5
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +8 -1
- package/dist/server.d.ts +11 -2
- package/dist/server.js +40 -2
- package/dist/styles.css +64 -80
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/default-dark.js +1 -0
- package/dist/tokens/themes/default-light.js +1 -0
- package/dist/tokens/themes/required.d.ts +1 -1
- package/dist/tokens/themes/required.js +1 -0
- package/dist/tokens/tokens.css +15 -0
- package/dist/ui/components/Checkbox.js +10 -2
- package/dist/ui/components/DateInput.js +8 -1
- package/dist/ui/components/Disclaimer.d.ts +19 -0
- package/dist/ui/components/Disclaimer.js +74 -0
- 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/components/tabs-overflow.js +64 -11
- 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/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +6 -0
- package/dist/ui/rich-text/RichText.style.css +48 -79
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +1831 -1822
|
@@ -4,7 +4,7 @@ import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
|
4
4
|
const CLS_OUTLINE_BTN = "appkit-button appkit-button--outline appkit-button--md flex w-full items-center gap-[var(--appkit-space-2)]";
|
|
5
5
|
import Link from "next/link.js";
|
|
6
6
|
import { Shield, Phone, Wallet, Mail, AlertOctagon, Flag, Eye, Users, Calendar, ExternalLink, FileText, MessageSquare, Link2, } from "lucide-react";
|
|
7
|
-
import { Alert, Anchor, Badge, Breadcrumb, Card, CardBody, CardHeader, Container, Div, EmptyState, Grid, Heading, Main, Row, Section, Span, Stack, Text, } from "../../../ui/index.js";
|
|
7
|
+
import { Alert, Anchor, Badge, Breadcrumb, Card, CardBody, CardHeader, Container, Disclaimer, Div, EmptyState, Grid, Heading, Main, Row, Section, Span, Stack, Text, } from "../../../ui/index.js";
|
|
8
8
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
9
9
|
import { SCAM_PLATFORM_LABELS, SCAMMER_STATUS_LABELS, SOCIAL_PLATFORM_LABELS, CONTEST_TYPE_LABELS, } from "../schemas/firestore.js";
|
|
10
10
|
import { SCAM_TYPE_LABELS, getScamType } from "../constants/scam-types.js";
|
|
@@ -32,7 +32,7 @@ function statusVariant(status) {
|
|
|
32
32
|
}
|
|
33
33
|
// ─── Sub-components ───────────────────────────────────────────────────────────
|
|
34
34
|
function ScammerHeaderCard({ scammer }) {
|
|
35
|
-
return (_jsx(Card, { variant: "elevated", padding: "lg", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { justify: "between", gap: "md", align: "start", wrap: true, children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, weight: "bold", size: "2xl", children: scammer.displayNames[0] }), scammer.displayNames.length > 1 && (_jsxs(Text, { variant: "secondary", size: "sm", children: ["Also known as: ", scammer.displayNames.slice(1).join(", ")] }))] }), _jsx(Badge, { variant: statusVariant(scammer.status), children: SCAMMER_STATUS_LABELS[scammer.status] ?? scammer.status })] }), _jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[scammer.scamType] ?? scammer.scamType }), _jsxs(Badge, { variant: "default", children: ["via ", SCAM_PLATFORM_LABELS[scammer.scamPlatform] ?? scammer.scamPlatform] }), scammer.tags.includes("repeat_offender") && (_jsx(Badge, { variant: "danger", children: "Repeat Offender" }))] }), _jsxs(Row, { gap: "md", wrap: true, children: [_jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Eye, { className: "h-4 w-4" }), scammer.views, " views"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Users, { className: "h-4 w-4" }), scammer.incidentCount + 1, " victim", scammer.incidentCount !== 0 ? "s" : "", " reported"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Calendar, { className: "h-4 w-4" }), "Reported ", formatDate(scammer.createdAt)] }), scammer.amountLost ? (_jsxs(Text, { className: "text-[color:var(--appkit-color-danger,theme(colors.red.600))]", size: "sm", weight: "medium", children: [formatCurrency(scammer.amountLost), " lost (primary incident)"] })) : null] })] }) }));
|
|
35
|
+
return (_jsx(Card, { variant: "elevated", padding: "lg", children: _jsxs(Stack, { gap: "md", children: [_jsx(Disclaimer, { id: "scam-registry" }), _jsxs(Row, { justify: "between", gap: "md", align: "start", wrap: true, children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, weight: "bold", size: "2xl", children: scammer.displayNames[0] }), scammer.displayNames.length > 1 && (_jsxs(Text, { variant: "secondary", size: "sm", children: ["Also known as: ", scammer.displayNames.slice(1).join(", ")] }))] }), _jsx(Badge, { variant: statusVariant(scammer.status), children: SCAMMER_STATUS_LABELS[scammer.status] ?? scammer.status })] }), _jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[scammer.scamType] ?? scammer.scamType }), _jsxs(Badge, { variant: "default", children: ["via ", SCAM_PLATFORM_LABELS[scammer.scamPlatform] ?? scammer.scamPlatform] }), scammer.tags.includes("repeat_offender") && (_jsx(Badge, { variant: "danger", children: "Repeat Offender" }))] }), _jsxs(Row, { gap: "md", wrap: true, children: [_jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Eye, { className: "h-4 w-4" }), scammer.views, " views"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Users, { className: "h-4 w-4" }), scammer.incidentCount + 1, " victim", scammer.incidentCount !== 0 ? "s" : "", " reported"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Calendar, { className: "h-4 w-4" }), "Reported ", formatDate(scammer.createdAt)] }), scammer.amountLost ? (_jsxs(Text, { className: "text-[color:var(--appkit-color-danger,theme(colors.red.600))]", size: "sm", weight: "medium", children: [formatCurrency(scammer.amountLost), " lost (primary incident)"] })) : null] })] }) }));
|
|
36
36
|
}
|
|
37
37
|
function ScammerIncidentsSection({ incidents }) {
|
|
38
38
|
return (_jsxs(Stack, { gap: "sm", children: [_jsx(Row, { justify: "between", align: "center", children: _jsxs(Heading, { level: 2, size: "base", weight: "semibold", children: ["Additional Incidents", incidents.length > 0 && ` (${incidents.length})`] }) }), incidents.length === 0 ? (_jsx(EmptyState, { icon: _jsx(FileText, { className: "h-8 w-8" }), title: "No additional incidents yet", description: "Other victims' verified reports linked to this profile will appear here." })) : (_jsx(Stack, { gap: "sm", children: incidents.map((inc) => (_jsx(Card, { variant: "outlined", padding: "md", children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", wrap: true, children: [_jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[inc.scamType] ?? inc.scamType }), _jsxs(Badge, { variant: "default", children: ["via ", SCAM_PLATFORM_LABELS[inc.scamPlatform] ?? inc.scamPlatform] })] }), _jsx(Text, { variant: "secondary", size: "xs", children: formatDate(inc.createdAt) })] }), inc.itemInvolved && (_jsxs(Text, { variant: "secondary", size: "xs", children: ["Item: ", inc.itemInvolved] })), inc.amountLost ? (_jsxs(Text, { className: "text-[color:var(--appkit-color-danger,theme(colors.red.600))]", size: "xs", weight: "medium", children: [formatCurrency(inc.amountLost), " lost"] })) : null, _jsx(Text, { variant: "secondary", className: "leading-relaxed", size: "sm", children: inc.description.length > 200
|
|
@@ -7,7 +7,7 @@ const CLS_ICON_SM = "h-3 w-3 shrink-0";
|
|
|
7
7
|
const CLS_ROW_CHEVRON = "h-4 w-4 shrink-0 transition group-hover:text-error text-[var(--appkit-color-text-faint)]";
|
|
8
8
|
const CLS_PILL_LINK = "inline-flex items-center gap-[var(--appkit-space-1)] rounded-full bg-error-surface dark:bg-error-surface px-[var(--appkit-space-2-5)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-medium text-error dark:text-error hover:bg-error-surface transition-colors";
|
|
9
9
|
import { Shield, Phone, Wallet, Mail, ChevronRight, Search } from "lucide-react";
|
|
10
|
-
import { Button, Container, Div, Form, Heading, Main, Section, Select, Text, Grid, Stack, Row, Card, Badge, Alert, EmptyState, Input, } from "../../../ui/index.js";
|
|
10
|
+
import { Button, Container, Disclaimer, Div, Form, Heading, Main, Section, Select, Text, Grid, Stack, Row, Card, Badge, Alert, EmptyState, Input, } from "../../../ui/index.js";
|
|
11
11
|
import { listVerifiedScammers } from "../actions/scam-actions.js";
|
|
12
12
|
import { SCAM_PLATFORM_LABELS } from "../schemas/firestore.js";
|
|
13
13
|
import { SCAM_TYPES, SCAM_TYPE_LABELS } from "../constants/scam-types.js";
|
|
@@ -61,7 +61,7 @@ export async function ScamRegistryView({ searchParams = {} }) {
|
|
|
61
61
|
const qs = p.toString();
|
|
62
62
|
return `/scams${qs ? `?${qs}` : ""}`;
|
|
63
63
|
}
|
|
64
|
-
return (_jsxs(Main, { children: [_jsx(Div, { className: "border-b appkit-alert--warning", rounded: "none", children: _jsx(Container, { padding: "y-xs", size: "xl", className: "", children: _jsxs(Alert, { variant: "warning", compact: true, children: ["All profiles on this page have been verified by our moderation team. If you recognise a scammer not listed here,", " ", _jsx(Link, { href: reportHref, className: "font-medium underline hover:no-underline", children: "report them" }), "."] }) }) }), _jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "xl", children: [_jsxs(Row, { justify: "between", gap: "md", align: "end", wrap: true, children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, weight: "semibold", size: "3xl", children: "Scam Registry" }), _jsx(Text, { variant: "secondary", size: "sm", children: "
|
|
64
|
+
return (_jsxs(Main, { children: [_jsx(Div, { className: "border-b appkit-alert--warning", rounded: "none", children: _jsx(Container, { padding: "y-xs", size: "xl", className: "", children: _jsxs(Alert, { variant: "warning", compact: true, children: ["All profiles on this page have been verified by our moderation team. If you recognise a scammer not listed here,", " ", _jsx(Link, { href: reportHref, className: "font-medium underline hover:no-underline", children: "report them" }), "."] }) }) }), _jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "xl", children: [_jsxs(Row, { justify: "between", gap: "md", align: "end", wrap: true, children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, weight: "semibold", size: "3xl", children: "Scam Registry" }), _jsx(Text, { variant: "secondary", size: "sm", children: "Reports of scams in India's collectibles community, submitted by buyers and sellers. Search by name, phone, or UPI." })] }), _jsxs(Link, { href: reportHref, className: "appkit-button appkit-button--danger appkit-button--md shrink-0", children: [_jsx(Shield, { className: "h-4 w-4" }), "Report a Scammer"] })] }), _jsx(Disclaimer, { id: "scam-registry" }), _jsx(Form, { method: "GET", children: _jsxs(Row, { wrap: true, gap: "sm", children: [_jsx(Div, { className: "flex-1 min-w-48", children: _jsx(Input, { type: "search", name: "q", defaultValue: query, placeholder: "Search name, phone, UPI ID, or email\u2026", icon: _jsx(Search, { className: "h-4 w-4" }) }) }), _jsx(Select, { name: "scamType", defaultValue: scamType, options: SCAM_TYPES.map((t) => ({ value: t.id, label: t.label })), placeholder: "All scam types" }), _jsx(Select, { name: "sort", defaultValue: sort, options: SORT_OPTIONS.map((o) => ({ value: o.value, label: o.label })) }), _jsx(Button, { type: "submit", variant: "primary", size: "sm", children: "Search" }), (query || scamType || sort !== "-createdAt") && (_jsx(Link, { href: ROUTES.PUBLIC.SCAMS, className: "rounded-lg border border-[var(--appkit-color-border)] px-[var(--appkit-space-4)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover transition-colors", children: "Clear" }))] }) }), scamType && SCAM_TYPE_LABELS[scamType] && (_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(Text, { variant: "secondary", size: "xs", children: "Filtering by:" }), _jsxs(Link, { href: buildHref({ scamType: "" }), className: CLS_PILL_LINK, children: [SCAM_TYPE_LABELS[scamType], _jsx(Span, { "aria-hidden": "true", children: "\u00D7" })] })] })), result.items.length === 0 ? (_jsx(EmptyState, { icon: _jsx(Shield, { className: "h-12 w-12" }), title: query || scamType
|
|
65
65
|
? "No verified scammers matched your filters"
|
|
66
66
|
: "No verified scammers in the registry yet", description: query || scamType
|
|
67
67
|
? "Try a different search or clear your filters."
|
|
@@ -103,7 +103,7 @@ export const CONSOLIDATED_TAB_SURFACES = [
|
|
|
103
103
|
tab: "navigation",
|
|
104
104
|
portal: "admin",
|
|
105
105
|
sectionPath: "Admin › System › Permissions Toggles",
|
|
106
|
-
requiredPermission: "settings:write",
|
|
106
|
+
requiredPermission: "admin:settings:write",
|
|
107
107
|
},
|
|
108
108
|
];
|
|
109
109
|
export function deriveConsolidatedTabEntries(hrefs) {
|
|
@@ -28,7 +28,7 @@ import { couponsRepository } from "../../promotions/repository/coupons.repositor
|
|
|
28
28
|
import { generateStoreSlug } from "../../stores/schemas/firestore.js";
|
|
29
29
|
import { siteSettingsRepository } from "../../admin/repository/site-settings.repository.js";
|
|
30
30
|
import { computePayoutDeduction } from "../../../_internal/shared/fees/calculator.js";
|
|
31
|
-
import { ordersTaxableValue, ordersShippingReimbursement, } from "../../../_internal/shared/checkout/order-math.js";
|
|
31
|
+
import { ordersTaxableValue, ordersShippingReimbursement, ordersPointsBorneByStore, } from "../../../_internal/shared/checkout/order-math.js";
|
|
32
32
|
import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
|
|
33
33
|
import { finalizeStagedMediaUrl, finalizeStagedMediaField, finalizeStagedMediaArray, } from "../../media/finalize.js";
|
|
34
34
|
import { StoreStatusValues } from "../../stores/schemas/firestore.js";
|
|
@@ -249,7 +249,19 @@ async function computeSellerEarnings(sellerId) {
|
|
|
249
249
|
* alone.
|
|
250
250
|
*/
|
|
251
251
|
const shippingReimbursement = ordersShippingReimbursement(eligibleOrders);
|
|
252
|
-
|
|
252
|
+
/*
|
|
253
|
+
* D46/D47 — the points programme, which the STORE funds at both ends
|
|
254
|
+
* (decisions 3 and 17): points issued on these orders plus points spent on
|
|
255
|
+
* them. Deducted outside the commission and gateway bases, for the inverse
|
|
256
|
+
* of postage's reason — the seller agreed to fund a discount, not to pay us a
|
|
257
|
+
* commission on the discount they funded.
|
|
258
|
+
*
|
|
259
|
+
* 🛑 Passed at all FOUR call sites, exactly like `shippingReimbursement`
|
|
260
|
+
* above. `ordersPointsFunded` existed with zero consumers before this, which
|
|
261
|
+
* meant the platform had silently been funding the earn.
|
|
262
|
+
*/
|
|
263
|
+
const pointsFunded = ordersPointsBorneByStore(eligibleOrders);
|
|
264
|
+
const deduction = computePayoutDeduction(grossAmount, siteSettings.commissions, shippingReimbursement, pointsFunded);
|
|
253
265
|
return {
|
|
254
266
|
eligibleOrders,
|
|
255
267
|
grossAmount,
|
|
@@ -257,6 +269,22 @@ async function computeSellerEarnings(sellerId) {
|
|
|
257
269
|
gatewayFee: deduction.gatewayFee,
|
|
258
270
|
gstAmount: deduction.gstOnFee,
|
|
259
271
|
netAmount: deduction.netAmount,
|
|
272
|
+
/*
|
|
273
|
+
* Surfaced so a seller can see WHY a payout is short of
|
|
274
|
+
* `gross − fees`. Two terms now move `netAmount` without appearing in
|
|
275
|
+
* `totalDeduction` — postage up, points down — and a net figure that
|
|
276
|
+
* cannot be reconstructed from the figures beside it reads as an error in
|
|
277
|
+
* our arithmetic rather than as the two things the seller agreed to.
|
|
278
|
+
*
|
|
279
|
+
* 🛑 Returned rather than written to `PayoutDocument`, matching
|
|
280
|
+
* `shippingReimbursement`, which is also not on that schema. One of these
|
|
281
|
+
* should probably be persisted eventually — a settled payout cannot be
|
|
282
|
+
* re-explained from a preview — but adding a document field with no reader
|
|
283
|
+
* is the pattern this very function just finished closing, so it waits for
|
|
284
|
+
* the admin surface that would read it.
|
|
285
|
+
*/
|
|
286
|
+
shippingReimbursement: deduction.shippingReimbursement,
|
|
287
|
+
pointsFunded: deduction.pointsFunded,
|
|
260
288
|
productCount: products.length,
|
|
261
289
|
commissions: siteSettings.commissions,
|
|
262
290
|
};
|
|
@@ -359,7 +387,9 @@ export async function bulkSellerOrder(userId, userRole, userDisplayName, userEma
|
|
|
359
387
|
* scheduled job pay differently for the same orders. */
|
|
360
388
|
const grossAmount = ordersTaxableValue(eligible);
|
|
361
389
|
const bulkSiteSettings = await siteSettingsRepository.getSingleton();
|
|
362
|
-
const bulkDeduction = computePayoutDeduction(grossAmount, bulkSiteSettings.commissions, ordersShippingReimbursement(eligible)
|
|
390
|
+
const bulkDeduction = computePayoutDeduction(grossAmount, bulkSiteSettings.commissions, ordersShippingReimbursement(eligible),
|
|
391
|
+
// The store-funded points, both ends — see the preview path above.
|
|
392
|
+
ordersPointsBorneByStore(eligible));
|
|
363
393
|
const payoutDoc = await payoutRepository.createPayout({
|
|
364
394
|
storeId: userId,
|
|
365
395
|
sellerName: userDisplayName,
|
|
@@ -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
|
}
|