@mohasinac/appkit 4.42.1 โ 4.42.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/offers/adapters.d.ts +29 -7
- package/dist/_internal/server/features/offers/adapters.js +14 -3
- package/dist/_internal/server/features/orders/adapters.js +54 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.js +1 -0
- package/dist/features/account/components/UserOrdersView.js +30 -2
- package/dist/features/account/hooks/useAddresses.d.ts +21 -6
- package/dist/features/account/hooks/useAddresses.js +1 -1
- package/dist/features/addresses/hooks/usePostalLookup.js +84 -12
- package/dist/features/admin/components/AdminCategoriesView.js +37 -2
- package/dist/features/admin/components/AdminOffersView.js +2 -2
- package/dist/features/admin/constants/filter-tabs.d.ts +25 -0
- package/dist/features/admin/constants/filter-tabs.js +30 -0
- package/dist/features/admin/hooks/useAdminListingData.d.ts +35 -0
- package/dist/features/admin/hooks/useAdminListingData.js +45 -9
- package/dist/features/cart/schemas/index.d.ts +15 -0
- package/dist/features/cart/schemas/index.js +2 -0
- package/dist/features/cart/types/index.d.ts +22 -0
- package/dist/features/categories/components/CategoriesIndexListing.js +19 -1
- package/dist/features/categories/components/CategoriesIndexPageView.js +14 -1
- package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +8 -0
- package/dist/features/classified/components/ClassifiedDetailPageView.js +1 -0
- package/dist/features/events/actions/event-actions.d.ts +13 -0
- package/dist/features/events/actions/event-actions.js +75 -1
- package/dist/features/events/repository/event-entry.repository.d.ts +17 -0
- package/dist/features/events/repository/event-entry.repository.js +36 -0
- package/dist/features/homepage/components/BrandsSection.js +1 -1
- package/dist/features/homepage/components/CustomerReviewsSection.js +1 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +1 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/media/upload/MediaUploadField.js +21 -3
- package/dist/features/orders/components/OrdersList.js +2 -1
- package/dist/features/orders/index.d.ts +1 -0
- package/dist/features/orders/index.js +1 -0
- package/dist/features/orders/types/index.d.ts +2 -0
- package/dist/features/orders/utils/order-ref.d.ts +45 -0
- package/dist/features/orders/utils/order-ref.js +58 -0
- package/dist/features/products/components/MakeOfferButton.d.ts +18 -1
- package/dist/features/products/components/MakeOfferButton.js +23 -1
- package/dist/features/products/components/ProductDetailPageView.d.ts +5 -0
- package/dist/features/products/components/ProductDetailPageView.js +3 -2
- package/dist/features/products/repository/products.repository.js +29 -2
- package/dist/features/seller/actions/offer-actions.js +32 -0
- package/dist/features/seller/components/QuickProductForm.js +15 -1
- package/dist/features/seller/components/SellerBidsView.js +3 -1
- package/dist/features/seller/components/SellerOffersView.js +2 -2
- package/dist/features/seller/components/SellerOrdersView.js +49 -6
- package/dist/features/seller/components/SellerProductsCards.js +23 -1
- package/dist/features/seller/components/SellerProductsView.js +27 -2
- package/dist/features/seller/hooks/useSellerListingData.d.ts +0 -1
- package/dist/features/seller/hooks/useSellerListingData.js +17 -19
- package/dist/features/shell/SectionForm.js +33 -2
- package/dist/features/tester/seed-data/_happy-path.d.ts +23 -0
- package/dist/features/tester/seed-data/_happy-path.js +710 -0
- package/dist/features/tester/seed-data/_money-flows.js +34 -23
- package/dist/features/tester/seed-data/authored/account-auth__profile-settings.js +22 -3
- package/dist/features/tester/seed-data/authored/addresses__address-filters.js +17 -12
- package/dist/features/tester/seed-data/authored/addresses__postal-lookup.js +7 -6
- package/dist/features/tester/seed-data/authored/addresses__postal-validation.js +10 -4
- package/dist/features/tester/seed-data/authored/addresses__state-picker.js +13 -8
- package/dist/features/tester/seed-data/authored/addresses__unban-request.js +49 -11
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +10 -6
- package/dist/features/tester/seed-data/authored/admin__bans-and-trust.js +71 -32
- package/dist/features/tester/seed-data/authored/admin__blog-faqs.js +80 -31
- package/dist/features/tester/seed-data/authored/admin__bug-hunter-rewards.js +62 -25
- package/dist/features/tester/seed-data/authored/admin__bulk-actions.js +61 -31
- package/dist/features/tester/seed-data/authored/admin__bundles.js +84 -37
- package/dist/features/tester/seed-data/authored/admin__buyer-data-admin.js +90 -57
- package/dist/features/tester/seed-data/authored/admin__catalog-listings.js +38 -26
- package/dist/features/tester/seed-data/authored/admin__category-brand-authoring.js +47 -28
- package/dist/features/tester/seed-data/authored/admin__classifieds-digitalcodes-live.js +118 -31
- package/dist/features/tester/seed-data/authored/admin__content-deletes.js +30 -24
- package/dist/features/tester/seed-data/authored/admin__content-marketing.js +83 -46
- package/dist/features/tester/seed-data/authored/admin__coupons.js +8 -8
- package/dist/features/tester/seed-data/authored/admin__events-raffles-spin.js +9 -9
- package/dist/features/tester/seed-data/authored/admin__firebase-function-effects.js +19 -4
- package/dist/features/tester/seed-data/authored/admin__media-watermark.js +77 -33
- package/dist/features/tester/seed-data/authored/admin__orders-fulfillment.js +5 -5
- package/dist/features/tester/seed-data/authored/admin__prize-draws-lotteries.js +29 -5
- package/dist/features/tester/seed-data/authored/admin__site-system.js +45 -23
- package/dist/features/tester/seed-data/authored/admin__uncovered-admin-pages.js +53 -28
- package/dist/features/tester/seed-data/authored/admin__users-trust.js +20 -20
- package/dist/features/tester/seed-data/authored/buying__bidding.js +19 -2
- package/dist/features/tester/seed-data/authored/buying__browsing-search.js +20 -5
- package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +28 -10
- package/dist/features/tester/seed-data/authored/buying__cart.js +10 -3
- package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +0 -16
- package/dist/features/tester/seed-data/authored/buying__my-orders.js +27 -5
- package/dist/features/tester/seed-data/authored/buying__offers.js +22 -7
- package/dist/features/tester/seed-data/authored/buying__order-detail-actions.js +3 -3
- package/dist/features/tester/seed-data/authored/buying__product-detail.js +35 -4
- package/dist/features/tester/seed-data/authored/buying__return-request.js +19 -6
- package/dist/features/tester/seed-data/authored/buying__reviews-pagination.js +36 -18
- package/dist/features/tester/seed-data/authored/buying__reviews.js +16 -1
- package/dist/features/tester/seed-data/authored/buying__user-dashboard-navigation.js +2 -1
- package/dist/features/tester/seed-data/authored/buying__user-uncovered-pages.js +16 -1
- package/dist/features/tester/seed-data/authored/community-support__public-profile.js +3 -2
- package/dist/features/tester/seed-data/authored/community-support__support-tickets.js +4 -4
- package/dist/features/tester/seed-data/authored/content-discovery__blog.js +19 -2
- package/dist/features/tester/seed-data/authored/content-discovery__category-brand-relations.js +4 -4
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +5 -5
- package/dist/features/tester/seed-data/authored/content-discovery__coupons.js +41 -6
- package/dist/features/tester/seed-data/authored/content-discovery__event-detail-subroutes.js +63 -8
- package/dist/features/tester/seed-data/authored/content-discovery__event-participation.js +29 -29
- package/dist/features/tester/seed-data/authored/content-discovery__events.js +2 -2
- package/dist/features/tester/seed-data/authored/content-discovery__faq-help.js +23 -1
- package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +6 -6
- package/dist/features/tester/seed-data/authored/content-discovery__search.js +1 -1
- package/dist/features/tester/seed-data/authored/content-discovery__store-reviews-aggregate.js +1 -1
- package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.js +31 -6
- package/dist/features/tester/seed-data/authored/cta-layout__dialog-footers.js +34 -10
- package/dist/features/tester/seed-data/authored/cta-layout__editor-action-bar.js +35 -8
- package/dist/features/tester/seed-data/authored/cta-layout__navbar-ctas.js +31 -9
- package/dist/features/tester/seed-data/authored/cta-layout__product-bottom-bar.js +29 -3
- package/dist/features/tester/seed-data/authored/design-ux__back-to-top-button.js +1 -1
- package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.js +10 -10
- package/dist/features/tester/seed-data/authored/design-ux__dashboard-layout.js +3 -3
- package/dist/features/tester/seed-data/authored/design-ux__footer-theme.js +2 -2
- package/dist/features/tester/seed-data/authored/design-ux__form-validation-errors.js +2 -2
- package/dist/features/tester/seed-data/authored/design-ux__general-design.js +4 -4
- package/dist/features/tester/seed-data/authored/design-ux__hand-mode-layout.js +1 -1
- package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.js +8 -8
- package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +2 -2
- package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +2 -2
- package/dist/features/tester/seed-data/authored/page-wiring__data-loss.js +39 -16
- package/dist/features/tester/seed-data/authored/page-wiring__detail-pages.js +43 -8
- package/dist/features/tester/seed-data/authored/page-wiring__drawer-pages.js +40 -14
- package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +31 -14
- package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +7 -6
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +14 -5
- package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +3 -3
- package/dist/features/tester/seed-data/authored/public-pages__help-how-it-works.js +1 -1
- package/dist/features/tester/seed-data/authored/public-pages__help-scams-guides-subpages.js +4 -2
- package/dist/features/tester/seed-data/authored/public-pages__legal-policy-pages.js +3 -3
- package/dist/features/tester/seed-data/authored/public-pages__newly-wired-browse-indexes.js +5 -5
- package/dist/features/tester/seed-data/authored/public-pages__store-tabs.js +5 -5
- package/dist/features/tester/seed-data/authored/public-pages__stores-sellers-directories.js +3 -3
- package/dist/features/tester/seed-data/authored/search-and-nav__employee-permissions.js +45 -11
- package/dist/features/tester/seed-data/authored/search-and-nav__header-search.js +57 -17
- package/dist/features/tester/seed-data/authored/search-and-nav__settings-deep-links.js +36 -5
- package/dist/features/tester/seed-data/authored/search-and-nav__sidebar-search.js +29 -4
- package/dist/features/tester/seed-data/authored/selling__become-seller.js +23 -0
- package/dist/features/tester/seed-data/authored/selling__final-sale-authoring.js +14 -13
- package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +14 -9
- package/dist/features/tester/seed-data/authored/selling__listing-edit-roundtrip.js +34 -7
- package/dist/features/tester/seed-data/authored/selling__listing-lifecycle.js +17 -7
- package/dist/features/tester/seed-data/authored/selling__listing-type-fields-roundtrip.js +9 -9
- package/dist/features/tester/seed-data/authored/selling__media-limits.js +9 -4
- package/dist/features/tester/seed-data/authored/selling__product-upload-details.js +3 -3
- package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.js +3 -3
- package/dist/features/tester/seed-data/authored/selling__sectionised-forms.js +25 -1
- package/dist/features/tester/seed-data/authored/selling__seller-analytics-payouts.js +27 -6
- package/dist/features/tester/seed-data/authored/selling__seller-bids-bundles-filters.js +19 -1
- package/dist/features/tester/seed-data/authored/selling__seller-catalog-org.js +63 -22
- package/dist/features/tester/seed-data/authored/selling__seller-custom-brands.js +9 -5
- package/dist/features/tester/seed-data/authored/selling__seller-guide.js +7 -6
- package/dist/features/tester/seed-data/authored/selling__seller-listing-types.js +3 -2
- package/dist/features/tester/seed-data/authored/selling__seller-marketing-extras.js +18 -18
- package/dist/features/tester/seed-data/authored/selling__seller-ops-comms.js +13 -12
- package/dist/features/tester/seed-data/authored/selling__seller-orders-at-scale.js +19 -3
- package/dist/features/tester/seed-data/authored/selling__seller-orders.js +22 -4
- package/dist/features/tester/seed-data/authored/selling__seller-shipping-payouts-setup.js +20 -8
- package/dist/features/tester/seed-data/authored/selling__store-dashboard-navigation.js +4 -4
- package/dist/features/tester/seed-data/authored/selling__store-uncovered-pages.js +2 -2
- package/dist/features/tester/seed-data/authored/seo__canonical-and-host.js +5 -5
- package/dist/features/tester/seed-data/authored/seo__og-images.js +1 -1
- package/dist/features/tester/seed-data/authored/seo__page-metadata.js +7 -7
- package/dist/features/tester/seed-data/authored/seo__sitemap-and-robots.js +3 -3
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +123 -21
- package/dist/features/tester/utils/phases.d.ts +17 -0
- package/dist/features/tester/utils/phases.js +29 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/seo/json-ld.d.ts +23 -0
- package/dist/seo/json-ld.js +3 -2
- package/dist/server.d.ts +1 -0
- package/dist/server.js +2 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/PaginatedSelect.js +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The short, buyer-facing reference for an order.
|
|
3
|
+
*
|
|
4
|
+
* ๐ WHY THIS EXISTS. Four sites independently rendered
|
|
5
|
+
* `order.id.slice(-8).toUpperCase()` โ the buyer's order view (twice), the
|
|
6
|
+
* invoice, and `OrdersList`. A fixed-width tail is the wrong shape for these
|
|
7
|
+
* ids: `generateOrderId` produces
|
|
8
|
+
*
|
|
9
|
+
* order-{itemCount}-{YYYYMMDD}-{rand6}
|
|
10
|
+
*
|
|
11
|
+
* whose final segment is **6** characters, so `slice(-8)` takes seven of them
|
|
12
|
+
* plus one extra โ and that extra is the last digit of the DATE. Measured on a
|
|
13
|
+
* real order, `order-2-20260929-ezq0pc` rendered as **`#9-EZQ0PC`**.
|
|
14
|
+
*
|
|
15
|
+
* Three things are wrong with that, in increasing order of cost:
|
|
16
|
+
*
|
|
17
|
+
* 1. It straddles a delimiter, so the reference is not a substring of anything
|
|
18
|
+
* meaningful โ the leading `9` looks like part of a structured reference and
|
|
19
|
+
* is a fragment of `20260929`.
|
|
20
|
+
* 2. Every order placed on the 9th, 19th or 29th of any month renders the same
|
|
21
|
+
* leading digit, and the 1st/11th/21st/31st share another. The digit adds no
|
|
22
|
+
* distinguishing information while looking like it does.
|
|
23
|
+
* 3. It is the number a buyer reads off their own screen and quotes to support,
|
|
24
|
+
* and it does not appear anywhere in the real document id. The payment page
|
|
25
|
+
* shows the full `order-2-20260929-ezq0pc`, so the same order presents two
|
|
26
|
+
* unrelated-looking references depending on which page you are on.
|
|
27
|
+
*
|
|
28
|
+
* The random suffix alone is the intended unique part, so that is what this
|
|
29
|
+
* returns. Taking the last hyphen-delimited SEGMENT rather than a character
|
|
30
|
+
* count also means the format can change without silently re-slicing.
|
|
31
|
+
*
|
|
32
|
+
* @tag domain:orders
|
|
33
|
+
* @tag layer:utils
|
|
34
|
+
* @tag access:isomorphic
|
|
35
|
+
* @tag sideEffects:none โ pure
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* `order-2-20260929-ezq0pc` โ `EZQ0PC`.
|
|
39
|
+
*
|
|
40
|
+
* Falls back to the whole id, uppercased, for anything that is not in the
|
|
41
|
+
* generator's shape (a legacy Firestore auto-id, say) โ a reference that is
|
|
42
|
+
* merely long is far better than one that is wrong, and an empty string would
|
|
43
|
+
* render as a bare `#`.
|
|
44
|
+
*/
|
|
45
|
+
export function shortOrderRef(orderId) {
|
|
46
|
+
if (!orderId)
|
|
47
|
+
return "";
|
|
48
|
+
const segments = orderId.split("-").filter(Boolean);
|
|
49
|
+
const last = segments[segments.length - 1] ?? orderId;
|
|
50
|
+
/*
|
|
51
|
+
* A single trailing segment that is purely numeric is a date, not a suffix โ
|
|
52
|
+
* that would be an id shaped `order-2-20260929` with no random part. Prefer
|
|
53
|
+
* the whole thing over presenting a date as a reference number.
|
|
54
|
+
*/
|
|
55
|
+
if (segments.length > 1 && /^\d+$/.test(last))
|
|
56
|
+
return orderId.toUpperCase();
|
|
57
|
+
return last.toUpperCase();
|
|
58
|
+
}
|
|
@@ -15,8 +15,25 @@ export interface MakeOfferButtonProps {
|
|
|
15
15
|
* form accepts, `makeOffer` accepts.
|
|
16
16
|
*/
|
|
17
17
|
bounds: OfferBounds;
|
|
18
|
+
/**
|
|
19
|
+
* The listing's owning store SLUG (`product.storeId`), so the owning seller
|
|
20
|
+
* is not offered a control for negotiating with themselves.
|
|
21
|
+
*
|
|
22
|
+
* ๐ Optional only because the two consumer pages must both keep compiling;
|
|
23
|
+
* pass it. Omitting it restores the old behaviour, which was to show the full
|
|
24
|
+
* offer form to the very seller who set the price โ measured on production as
|
|
25
|
+
* tyson@beybladearena.in on a listing sold by his own store, complete with
|
|
26
|
+
* the buyer-facing line "The seller will accept, decline, or suggest a
|
|
27
|
+
* counter price".
|
|
28
|
+
*
|
|
29
|
+
* This is a UI courtesy, NOT the authorization boundary โ `makeOffer` refuses
|
|
30
|
+
* a self-offer server-side, which is where the actual rule lives (Root Cause
|
|
31
|
+
* #46: a hidden button is not a gate). Hiding it here only stops a seller
|
|
32
|
+
* filling in a form that was always going to be rejected.
|
|
33
|
+
*/
|
|
34
|
+
listingStoreId?: string;
|
|
18
35
|
/** Called with (productId, offerAmount, note?). Must return void or throw on error. */
|
|
19
36
|
onMakeOffer: (productId: string, amount: number, note?: string) => Promise<void>;
|
|
20
37
|
className?: string;
|
|
21
38
|
}
|
|
22
|
-
export declare function MakeOfferButton({ productId, listedPrice, currency, bounds, onMakeOffer, className, }: MakeOfferButtonProps): React.JSX.Element;
|
|
39
|
+
export declare function MakeOfferButton({ productId, listedPrice, currency, bounds, listingStoreId, onMakeOffer, className, }: MakeOfferButtonProps): React.JSX.Element | null;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useTransition } from "react";
|
|
4
4
|
import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices";
|
|
5
|
+
import { useOptionalSession } from "../../../react/contexts/SessionContext";
|
|
5
6
|
import { Button, Div, LoginRequiredModal, Modal, Span, Stack, Text, } from "../../../ui";
|
|
6
7
|
import { Form } from "../../../ui/components/Form";
|
|
7
8
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
@@ -28,11 +29,22 @@ const CLS_PENDING_BODY = "text-[length:var(--appkit-text-xs)] text-warning dark:
|
|
|
28
29
|
function isActiveOfferError(msg) {
|
|
29
30
|
return msg.includes("active offer") || msg.includes("ACTIVE_OFFER");
|
|
30
31
|
}
|
|
31
|
-
export function MakeOfferButton({ productId, listedPrice, currency, bounds, onMakeOffer, className = "", }) {
|
|
32
|
+
export function MakeOfferButton({ productId, listedPrice, currency, bounds, listingStoreId, onMakeOffer, className = "", }) {
|
|
32
33
|
const [state, setState] = useState("idle");
|
|
33
34
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
34
35
|
const { canSeePrices } = useCanSeePrices();
|
|
36
|
+
const session = useOptionalSession();
|
|
35
37
|
const [isPending, startTransition] = useTransition();
|
|
38
|
+
/*
|
|
39
|
+
* `UserDocument.storeId` and `ProductDocument.storeId` are both the store
|
|
40
|
+
* SLUG, so this is a same-namespace comparison (unlike the uid-vs-slug trap
|
|
41
|
+
* that made `getSellerProductAction`'s ownership check always fail โ Root
|
|
42
|
+
* Cause #98). `useOptionalSession` rather than `useSession` because library
|
|
43
|
+
* code that adjusts for the viewer must not require a provider, and no
|
|
44
|
+
* provider must fail CLOSED โ no session means no owner, i.e. show the
|
|
45
|
+
* button, which is correct for the signed-out visitor it describes.
|
|
46
|
+
*/
|
|
47
|
+
const viewerOwnsListing = Boolean(listingStoreId) && session?.user?.storeId === listingStoreId;
|
|
36
48
|
const fmt = (n) => (currency ? formatCurrency(n, currency) : `โน${n.toLocaleString()}`);
|
|
37
49
|
// The SAME bounds the server enforces โ passed in, never recomputed, so the
|
|
38
50
|
// input can neither seed nor accept an amount `makeOffer` would reject.
|
|
@@ -80,6 +92,16 @@ export function MakeOfferButton({ productId, listedPrice, currency, bounds, onMa
|
|
|
80
92
|
}
|
|
81
93
|
});
|
|
82
94
|
}
|
|
95
|
+
/*
|
|
96
|
+
* Checked AFTER the success/pending panels below would have been, but before
|
|
97
|
+
* the idle button โ placed here so it cannot hide a panel reporting an offer
|
|
98
|
+
* this viewer already sent. In practice an owner never reaches those states
|
|
99
|
+
* (the server refuses), so ordering only matters if `listingStoreId` is ever
|
|
100
|
+
* wrong: then the worst outcome is a visible panel rather than a silently
|
|
101
|
+
* swallowed one.
|
|
102
|
+
*/
|
|
103
|
+
if (viewerOwnsListing)
|
|
104
|
+
return null;
|
|
83
105
|
if (state === "success") {
|
|
84
106
|
return (_jsxs(Div, { className: `${CLS_SUCCESS_BOX} ${className}`, children: [_jsx(Span, { size: "lg", children: "\uD83C\uDF89" }), _jsx(Text, { className: "text-success", size: "sm", weight: "medium", children: bounds.isBuyRequest ? "Request sent!" : "Offer sent!" }), _jsxs(Text, { className: "text-success", size: "xs", children: ["The seller will review your ", bounds.isBuyRequest ? "request" : "offer", " and respond shortly."] })] }));
|
|
85
107
|
}
|
|
@@ -24,6 +24,11 @@ export interface ProductDetailPageViewProps {
|
|
|
24
24
|
minOfferPercent: number;
|
|
25
25
|
/** Resolved by `resolveOfferBounds()` โ the same rule `makeOffer` enforces. */
|
|
26
26
|
bounds: OfferBounds;
|
|
27
|
+
/**
|
|
28
|
+
* The owning store's SLUG, so the control can suppress itself for the
|
|
29
|
+
* seller who owns the listing. Same namespace as `UserDocument.storeId`.
|
|
30
|
+
*/
|
|
31
|
+
listingStoreId?: string;
|
|
27
32
|
}) => React.ReactNode;
|
|
28
33
|
/**
|
|
29
34
|
* Renders the primary action buttons (Buy Now / Add to Cart / Wishlist).
|
|
@@ -204,7 +204,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
204
204
|
price: product.price,
|
|
205
205
|
storeId: product.storeId,
|
|
206
206
|
storeName: product.storeName,
|
|
207
|
-
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsx(ProductDetailView, { renderBreadcrumb: () => (_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRODUCTS), className: CLS_BREADCRUMB_LINK, children: "Products" }), (primaryCategorySlug || category) && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(primaryCategorySlug ?? category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), subcategory && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { transform: "capitalize", children: subcategory })] }))] }), _jsx(ShareButton, { title: title || undefined })] })), renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title || undefined })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [condition && (_jsx(Span, { size: "xs", weight: "medium", className: "mb-2 inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", transform: "capitalize", children: condition === "new"
|
|
207
|
+
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsx(ProductDetailView, { renderBreadcrumb: () => (_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRODUCTS), className: CLS_BREADCRUMB_LINK, children: "Products" }), (primaryCategorySlug || category) && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(primaryCategorySlug ?? category)), className: CLS_BREADCRUMB_LINK, children: categoryNames[0] || categoryName || category })] })), subcategory && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { transform: "capitalize", children: subcategory })] }))] }), _jsx(ShareButton, { title: title || undefined })] })), renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title || undefined })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [condition && (_jsx(Span, { size: "xs", weight: "medium", className: "mb-2 inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", transform: "capitalize", children: condition === "new"
|
|
208
208
|
? "Brand New"
|
|
209
209
|
: condition === "like_new"
|
|
210
210
|
? "Like New"
|
|
@@ -250,6 +250,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
250
250
|
price,
|
|
251
251
|
currency,
|
|
252
252
|
minOfferPercent,
|
|
253
|
+
listingStoreId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
253
254
|
bounds: resolveOfferBounds({
|
|
254
255
|
listingType: normalizeListingType(p),
|
|
255
256
|
listedPrice: price,
|
|
@@ -272,7 +273,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
272
273
|
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
273
274
|
})) })), renderBundleSection: bundleMemberships.length > 0
|
|
274
275
|
? () => (_jsxs(Div, { className: CLS_BUNDLE_BOX, children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "sm", children: [_jsx(Span, { size: "base", className: CLS_BUNDLE_ICON, "aria-hidden": "true", children: "\uD83D\uDCE6" }), _jsxs(Text, { className: CLS_BUNDLE_TITLE, children: ["Part of ", bundleMemberships.length === 1 ? "a bundle" : `${bundleMemberships.length} bundles`] })] }), _jsx(Stack, { gap: "3", children: bundleMemberships.map((b) => (_jsxs(Div, { className: CLS_BUNDLE_ROW, children: [_jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { className: CLS_BUNDLE_LABEL, children: "Included in bundle" }), _jsx(Text, { className: "truncate", color: "primary", size: "sm", weight: "semibold", children: b.title })] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLE_DETAIL(b.id)), className: CLS_BUNDLE_CTA, children: "View Bundle \u2192" })] }, b.id))) })] }))
|
|
275
|
-
: undefined, renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] })) }), renderPrimaryActions
|
|
276
|
+
: undefined, renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryNames[0] || categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] })) }), renderPrimaryActions
|
|
276
277
|
? renderPrimaryActions({
|
|
277
278
|
productId: product.id,
|
|
278
279
|
productSlug: product.slug ?? slug,
|
|
@@ -130,8 +130,35 @@ export class ProductRepository extends BaseRepository {
|
|
|
130
130
|
async deriveTaxonomy(input) {
|
|
131
131
|
const out = {};
|
|
132
132
|
const cats = this.db.collection(CATEGORIES_COLLECTION);
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
/*
|
|
134
|
+
* ๐ THE SCALAR `category` WINS. It used to be the other way round, and a
|
|
135
|
+
* seller could not re-categorise a listing at all.
|
|
136
|
+
*
|
|
137
|
+
* `categorySlugs` is a value this function DERIVES; `category` is the leaf a
|
|
138
|
+
* category picker actually writes. The seller edit form sends both โ its
|
|
139
|
+
* draft carries the loaded product's `categorySlugs` untouched while the
|
|
140
|
+
* picker updates `category` โ so a payload mid-edit reads, measured off the
|
|
141
|
+
* real request:
|
|
142
|
+
*
|
|
143
|
+
* "categorySlugs": ["category-burst-superking"] <- STALE, derived
|
|
144
|
+
* "category": "category-original-plastic-gen" <- what the user picked
|
|
145
|
+
*
|
|
146
|
+
* Preferring `categorySlugs[0]` therefore resolved the OLD leaf, re-derived
|
|
147
|
+
* the OLD chain from it, and then line `out.category = leaf` wrote the old
|
|
148
|
+
* value back OVER the user's choice. The save returned success, the form
|
|
149
|
+
* re-opened on the original category, the product page kept its old chips and
|
|
150
|
+
* the new category page never listed the item โ with nothing logged anywhere,
|
|
151
|
+
* because from this function's point of view it had resolved a leaf perfectly
|
|
152
|
+
* well. It was only ever the WRONG leaf.
|
|
153
|
+
*
|
|
154
|
+
* Reversing the precedence is safe for every other caller: anyone who passes
|
|
155
|
+
* `categorySlugs` without a `category` (the seed, an admin PATCH) still falls
|
|
156
|
+
* through to it, and anyone passing a consistent pair gets the same answer
|
|
157
|
+
* either way. The two can only disagree when one of them is stale, and the
|
|
158
|
+
* derived one is the one that goes stale.
|
|
159
|
+
*/
|
|
160
|
+
const leaf = input.category ||
|
|
161
|
+
(Array.isArray(input.categorySlugs) && input.categorySlugs[0]) ||
|
|
135
162
|
null;
|
|
136
163
|
/*
|
|
137
164
|
* The brand selector's option value is the brand ROW's id (`brand-beyblade`),
|
|
@@ -48,6 +48,38 @@ export async function makeOffer(userId, userEmail, input) {
|
|
|
48
48
|
const product = await productRepository.findById(productId);
|
|
49
49
|
if (!product)
|
|
50
50
|
throw new NotFoundError(ERROR_MESSAGES.PRODUCT.NOT_FOUND);
|
|
51
|
+
/*
|
|
52
|
+
* ๐ A seller may not offer on their OWN listing. There was no such check.
|
|
53
|
+
*
|
|
54
|
+
* Measured on production as tyson@beybladearena.in, who owns
|
|
55
|
+
* store-beyblade-arena: product-beyblade-burst-valkyrie โ sold by that very
|
|
56
|
+
* store โ offered a live 'Make Offer' control, and clicking it opened the
|
|
57
|
+
* full form ("Listed at โน999 ยท Minimum offer: โน699.3") with a working
|
|
58
|
+
* 'Send offer of โน899.1' button and the buyer-facing line "The seller will
|
|
59
|
+
* accept, decline, or suggest a counter price" shown TO that seller.
|
|
60
|
+
*
|
|
61
|
+
* The three gates above it are all real but none of them is about identity:
|
|
62
|
+
* the type's `canMakeOffer`, the seller's `allowOffers` opt-in, and the
|
|
63
|
+
* amount bounds. So a self-offer would have been accepted and written, and
|
|
64
|
+
* the same person would then appear on both sides of a negotiation โ able to
|
|
65
|
+
* accept their own offer and, through the offer lane, mint a locked cart line
|
|
66
|
+
* at any price down to the minimum floor. That is a pricing bypass, not a
|
|
67
|
+
* cosmetic one.
|
|
68
|
+
*
|
|
69
|
+
* The check is HERE rather than only in the UI on purpose (Root Cause #46): a
|
|
70
|
+
* hidden button is not an authorization boundary, and the control was already
|
|
71
|
+
* reachable. `product.storeId` is the store SLUG while `userId` is an Auth
|
|
72
|
+
* uid โ two different namespaces by design โ so the owner is resolved through
|
|
73
|
+
* `findByOwnerId`, exactly as `sellerUpdateProduct` does.
|
|
74
|
+
*
|
|
75
|
+
* Admins are not exempted: there is no legitimate reason for anyone to
|
|
76
|
+
* negotiate with themselves, and an admin wanting to adjust a price has the
|
|
77
|
+
* listing editor.
|
|
78
|
+
*/
|
|
79
|
+
const callerStore = await storeRepository.findByOwnerId(userId);
|
|
80
|
+
if (callerStore && callerStore.id === product.storeId) {
|
|
81
|
+
throw new ValidationError("You can't make an offer on your own listing โ edit its price instead.");
|
|
82
|
+
}
|
|
51
83
|
const listingType = normalizeListingType(product);
|
|
52
84
|
// Some listing types cannot be negotiated at all โ an auction already has
|
|
53
85
|
// bidding, a prize-draw sells fixed-price entries. This was never checked, so
|
|
@@ -13,6 +13,16 @@ function toRupeesString(price) {
|
|
|
13
13
|
function fromRupeesString(rupeeStr) {
|
|
14
14
|
return Math.round((parseFloat(rupeeStr) || 0) * 100) / 100;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Mirrors `productBaseSchema.description` (`z.string().min(20).max(5000)`).
|
|
18
|
+
*
|
|
19
|
+
* ๐ This validator already mirrored every other server constraint โ title
|
|
20
|
+
* length, price, image, category โ and simply omitted description, so the
|
|
21
|
+
* server was the first thing to notice and it answered in Zod's own words:
|
|
22
|
+
* "Invalid input: expected string, received undefined", printed under a field
|
|
23
|
+
* whose placeholder read "(optional)".
|
|
24
|
+
*/
|
|
25
|
+
const DESCRIPTION_MIN = 20;
|
|
16
26
|
function validate(values) {
|
|
17
27
|
const errors = {};
|
|
18
28
|
if (!values.title?.trim() || (values.title.trim().length < 3)) {
|
|
@@ -27,6 +37,10 @@ function validate(values) {
|
|
|
27
37
|
if (!values.category?.trim()) {
|
|
28
38
|
errors.category = "Category is required";
|
|
29
39
|
}
|
|
40
|
+
const description = values.description?.trim() ?? "";
|
|
41
|
+
if (description.length < DESCRIPTION_MIN) {
|
|
42
|
+
errors.description = `Description must be at least ${DESCRIPTION_MIN} characters`;
|
|
43
|
+
}
|
|
30
44
|
return errors;
|
|
31
45
|
}
|
|
32
46
|
export function QuickProductForm({ values, onChange, onPublish, onSave, onSwitchToFull, isLoading, renderCategorySelector, onUploadImage, }) {
|
|
@@ -51,5 +65,5 @@ export function QuickProductForm({ values, onChange, onPublish, onSave, onSwitch
|
|
|
51
65
|
return (_jsxs(Stack, { gap: "lg", className: `${__P.p5}`, children: [_jsx(Div, { children: _jsx(Text, { size: "sm", color: "muted", children: "Quick add \u2014 fill the essentials and publish. You can add more details later." }) }), touched && Object.keys(errors).length > 0 && (_jsx(Alert, { variant: "error", children: "Please fix the highlighted fields before publishing." })), _jsx(FormField, { name: "title", label: "Product Name", type: "text", value: values.title ?? "", onChange: (v) => onChange({ title: v }), placeholder: "e.g. Charizard Base Set PSA 9", required: true, error: touched ? errors.title : undefined }), renderCategorySelector ? (_jsxs(Div, { children: [_jsx(Text, { className: "mb-1", color: "muted", size: "sm", weight: "medium", children: "Category" }), renderCategorySelector({
|
|
52
66
|
value: values.category ?? "",
|
|
53
67
|
onChange: (v) => onChange({ category: v }),
|
|
54
|
-
}), touched && errors.category && (_jsx(Text, { className: "text-[var(--appkit-color-error)] mt-1", size: "xs", children: errors.category }))] })) : (_jsx(FormField, { name: "category", label: "Category", type: "text", value: values.category ?? "", onChange: (v) => onChange({ category: v }), placeholder: "e.g. Trading Cards", error: touched ? errors.category : undefined })), _jsx(FormField, { name: "price", label: "Price (\u20B9)", type: "number", value: toRupeesString(values.price), onChange: (v) => onChange({ price: fromRupeesString(v) }), placeholder: "e.g. 499", required: true, error: touched ? errors.price : undefined }), _jsx(FormField, { name: "mainImage", label: "Product Image", type: "image", value: values.mainImage ?? "", onChange: (v) => onChange({ mainImage: v }), onUpload: onUploadImage, required: true, error: touched ? errors.mainImage : undefined }), _jsx(FormField, { name: "description", label: "Description", type: "textarea", value: values.description ?? "", onChange: (v) => onChange({ description: v }), placeholder: "
|
|
68
|
+
}), touched && errors.category && (_jsx(Text, { className: "text-[var(--appkit-color-error)] mt-1", size: "xs", children: errors.category }))] })) : (_jsx(FormField, { name: "category", label: "Category", type: "text", value: values.category ?? "", onChange: (v) => onChange({ category: v }), placeholder: "e.g. Trading Cards", error: touched ? errors.category : undefined })), _jsx(FormField, { name: "price", label: "Price (\u20B9)", type: "number", value: toRupeesString(values.price), onChange: (v) => onChange({ price: fromRupeesString(v) }), placeholder: "e.g. 499", required: true, error: touched ? errors.price : undefined }), _jsx(FormField, { name: "mainImage", label: "Product Image", type: "image", value: values.mainImage ?? "", onChange: (v) => onChange({ mainImage: v }), onUpload: onUploadImage, required: true, error: touched ? errors.mainImage : undefined }), _jsx(FormField, { name: "description", label: "Description", type: "textarea", required: true, value: values.description ?? "", onChange: (v) => onChange({ description: v }), placeholder: "What is it, what condition is it in, what is included? (at least 20 characters)", rows: 3, error: touched ? errors.description : undefined }), _jsx(FormField, { name: "stockQuantity", label: "Stock Quantity", type: "number", value: String(values.stockQuantity ?? 1), onChange: (v) => onChange({ stockQuantity: Math.max(0, parseInt(v, 10) || 0) }), placeholder: "1", hint: "How many units are available" }), _jsxs(Stack, { gap: "3", padding: "t-xs", children: [_jsx(FormErrorSummary, {}), _jsxs(Row, { gap: "3", children: [_jsx(Button, { variant: "primary", onClick: handlePublish, disabled: isLoading, className: "flex-1", children: isLoading ? "Publishing..." : "Publish" }), _jsx(Button, { variant: "secondary", onClick: handleSave, disabled: isLoading, children: "Save Draft" })] }), _jsx(Button, { variant: "ghost", size: "sm", textSize: "xs", onClick: onSwitchToFull, className: "self-center", children: "Show all fields (advanced)" })] })] }));
|
|
55
69
|
}
|
|
@@ -10,7 +10,9 @@ import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
|
10
10
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
11
11
|
import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers";
|
|
12
12
|
import { SELLER_BID_STATUS_TABS } from "../../admin/constants/filter-tabs";
|
|
13
|
-
import { toRecordArray,
|
|
13
|
+
import { toRecordArray, toCurrency, toStringValue } from "../hooks/useSellerListingData";
|
|
14
|
+
// Defining module (Root Cause #18) โ the seller-side duplicate was deleted for drift.
|
|
15
|
+
import { toRelativeDate } from "../../admin/hooks/useAdminListingData";
|
|
14
16
|
import { DataTable } from "../../admin/components/DataTable";
|
|
15
17
|
import { DataListingView } from "../../admin/components/DataListingView";
|
|
16
18
|
import { TextLink } from "../../../ui";
|
|
@@ -8,7 +8,7 @@ import { QuickFormDrawer } from "../../shell/QuickFormDrawer";
|
|
|
8
8
|
import { OfferPhaseTimeline } from "./OfferPhaseTimeline";
|
|
9
9
|
import { counterOfferFormSchema } from "../schemas/offer-forms";
|
|
10
10
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
11
|
-
import { SELLER_OFFER_STATUS_TABS } from "../../admin/constants/filter-tabs";
|
|
11
|
+
import { SELLER_OFFER_STATUS_TABS, offerStatusLabel } from "../../admin/constants/filter-tabs";
|
|
12
12
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
13
13
|
import { toRecordArray, toRelativeDate, toCurrency, toStringValue, } from "../../admin/hooks/useAdminListingData";
|
|
14
14
|
import { DataListingView } from "../../admin/components/DataListingView";
|
|
@@ -49,7 +49,7 @@ export function SellerOffersView({ children, onAcceptOffer, onRejectOffer, onCou
|
|
|
49
49
|
`Listed: ${toCurrency(item.listedPrice)}`,
|
|
50
50
|
toStringValue(item.buyerName ?? "Unknown buyer", "Unknown buyer"),
|
|
51
51
|
].join(" ยท "),
|
|
52
|
-
status: toStringValue(item.status, "
|
|
52
|
+
status: offerStatusLabel(toStringValue(item.status, "pending"), "seller"),
|
|
53
53
|
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
54
54
|
detail: item,
|
|
55
55
|
})),
|
|
@@ -22,7 +22,9 @@ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
|
|
|
22
22
|
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
23
23
|
import { PhysicalLocationModal } from "./PhysicalLocationModal";
|
|
24
24
|
import { ROUTES } from "../../../constants";
|
|
25
|
-
import { toRecordArray,
|
|
25
|
+
import { toRecordArray, toCurrency, toStringValue } from "../hooks/useSellerListingData";
|
|
26
|
+
// Defining module (Root Cause #18) โ the seller-side duplicate was deleted for drift.
|
|
27
|
+
import { toRelativeDate } from "../../admin/hooks/useAdminListingData";
|
|
26
28
|
import { DataListingView } from "../../admin/components/DataListingView";
|
|
27
29
|
import { MediaImage } from "../../media/MediaImage";
|
|
28
30
|
const __O = {
|
|
@@ -119,7 +121,22 @@ export function SellerOrderDetailPanel({ orderId, apiBase, onClose, }) {
|
|
|
119
121
|
setDraft({
|
|
120
122
|
status: "",
|
|
121
123
|
trackingNumber: o.trackingNumber ?? "",
|
|
122
|
-
|
|
124
|
+
/*
|
|
125
|
+
* ๐ `shippingCarrier` is the stored field. `OrderDocument` declares
|
|
126
|
+
* it at line ~422 and declares no `carrier` at all.
|
|
127
|
+
*
|
|
128
|
+
* The WRITE below already gets this right (`payload.shippingCarrier`),
|
|
129
|
+
* so the carrier has been persisting correctly all along โ verified
|
|
130
|
+
* against the live endpoint after shipping an order: `shippingCarrier:
|
|
131
|
+
* "QA Carrier"`, `carrier: undefined`. Only the read-back was wrong, so
|
|
132
|
+
* reopening a shipped order showed an EMPTY Carrier box next to a
|
|
133
|
+
* populated tracking number. A seller would reasonably retype it, and
|
|
134
|
+
* the diff-against-loaded-state payload means retyping the same value
|
|
135
|
+
* would then look like no change at all.
|
|
136
|
+
*
|
|
137
|
+
* Not a lost write โ a field the form could not see.
|
|
138
|
+
*/
|
|
139
|
+
carrier: o.shippingCarrier ?? o.carrier ?? "",
|
|
123
140
|
trackingUrl: o.trackingUrl ?? "",
|
|
124
141
|
});
|
|
125
142
|
})
|
|
@@ -191,7 +208,10 @@ export function SellerOrderDetailPanel({ orderId, apiBase, onClose, }) {
|
|
|
191
208
|
setDraft({
|
|
192
209
|
status: "",
|
|
193
210
|
trackingNumber: next.trackingNumber ?? "",
|
|
194
|
-
|
|
211
|
+
// Same stored-field name as the initial load above โ this re-seed is
|
|
212
|
+
// explicitly "against the saved state", so reading the wrong field here
|
|
213
|
+
// makes every subsequent diff think the carrier had been cleared.
|
|
214
|
+
carrier: next.shippingCarrier ?? next.carrier ?? "",
|
|
195
215
|
trackingUrl: next.trackingUrl ?? "",
|
|
196
216
|
});
|
|
197
217
|
}
|
|
@@ -203,9 +223,32 @@ export function SellerOrderDetailPanel({ orderId, apiBase, onClose, }) {
|
|
|
203
223
|
setSaving(false);
|
|
204
224
|
}
|
|
205
225
|
};
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
226
|
+
/*
|
|
227
|
+
* ๐ `shippingAddress` is sometimes a STRING, and destructuring one yields
|
|
228
|
+
* nothing but silence.
|
|
229
|
+
*
|
|
230
|
+
* Measured on both a seeded order and one placed through real checkout,
|
|
231
|
+
* the single-order store endpoint returns it pre-formatted:
|
|
232
|
+
*
|
|
233
|
+
* "Mock User 3, 123 Stadium Lane, Vijay Nagar, Indore, Madhya Pradesh, 452010, India"
|
|
234
|
+
*
|
|
235
|
+
* Reading `.addressLine1` / `.city` / `.state` / `.pincode` off a string gives
|
|
236
|
+
* four undefineds, `filter(Boolean)` drops them all, and `addrLine` became "".
|
|
237
|
+
* The block below renders only when `addrLine` is truthy โ so the seller was
|
|
238
|
+
* shown NO shipping destination at all for an order they are expected to post,
|
|
239
|
+
* with no error and no empty state. (`Object.keys()` on it returns "0".."80",
|
|
240
|
+
* which is the tell: those are string indices, not fields.)
|
|
241
|
+
*
|
|
242
|
+
* Handle both shapes rather than picking one: the object form is what the
|
|
243
|
+
* schema declares and what newer writes produce, and a seller needs the
|
|
244
|
+
* address either way.
|
|
245
|
+
*/
|
|
246
|
+
const rawAddr = order?.shippingAddress;
|
|
247
|
+
const addr = (typeof rawAddr === "object" && rawAddr !== null ? rawAddr : {});
|
|
248
|
+
const addrLine = typeof rawAddr === "string" && rawAddr.trim()
|
|
249
|
+
? rawAddr.trim()
|
|
250
|
+
: [addr.addressLine1, addr.city, addr.state, addr.pincode].filter(Boolean).join(", ");
|
|
251
|
+
return (_jsxs(_Fragment, { children: [loading && (_jsx(Row, { align: "center", 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" }) })), fetchError && (_jsx(Div, { textSize: "sm", className: "mx-4 mt-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "lg", children: fetchError })), order && !loading && (_jsxs(Stack, { gap: "none", children: [_jsxs(Stack, { className: `flex-1 ${__O.yAuto}`, gap: "5", padding: "md", children: [_jsxs(Row, { align: "center", justify: "between", children: [_jsx(Badge, { variant: STATUS_BADGE_VARIANT[order.status ?? ""] ?? "default", children: order.status ?? "Unknown" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: toRelativeDate(order.createdAt) })] }), (order.items ?? []).length > 0 && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-primary)] mb-2", weight: "semibold", children: "Items" }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.items ?? []).map((item, i) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Row, { align: "center", gap: "sm", className: "min-w-0", children: [_jsx(Div, { className: "h-10 w-10 shrink-0", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title ?? "Order item", size: "thumbnail" }) }), _jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.productTitle ?? item.title ?? item.productId ?? "Item" }), _jsxs(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: ["Qty: ", item.quantity ?? 1] })] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "medium", children: toCurrency(item.totalPrice ?? item.unitPrice ?? item.price ?? 0) })] }, i))) })] })), _jsx(OrderAddonBadges, { order: order, variant: "detail" }), order.giftWrapAddon && (_jsxs(Div, { className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)]", surface: "subtle", padding: "inline", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "\uD83C\uDF81 Gift wrap requested" }), order.giftWrapMessage && (_jsxs(Text, { size: "sm", className: "mt-1 text-[var(--appkit-color-text-secondary)]", children: ["\u201C", order.giftWrapMessage, "\u201D"] }))] })), _jsxs(Row, { surface: "muted", padding: "inline", align: "center", justify: "between", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Total" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-primary)]", weight: "bold", children: toCurrency(order.totalAmount ?? order.totalPrice ?? 0) })] }), addrLine && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "mb-1", weight: "semibold", children: "Shipping address" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: [String(addr.fullName ?? ""), addrLine].filter(Boolean).join(" ยท ") })] })), order.paymentMethod && (_jsxs(Div, { children: [_jsxs(Row, { align: "center", justify: "between", className: "mb-1", gap: "3", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Payment" }), isManualPaymentMethod(order.paymentMethod) && (_jsx(Badge, { variant: sellerPaymentBadge(order).variant, children: sellerPaymentBadge(order).label }))] }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", transform: "capitalize", children: order.paymentMethod }), isManualPaymentMethod(order.paymentMethod) && order.paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", className: "mt-1", children: ["UTR: ", order.paymentTransactionId] }))] })), order.emiEnabled && (_jsxs(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", children: [_jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsxs(Text, { size: "sm", weight: "semibold", children: ["EMI plan ", order.emiTenureMonths ? `ยท ${order.emiTenureMonths} months` : ""] }), _jsx(Badge, { variant: order.emiComplete ? "success" : "warning", children: order.emiComplete ? "Fully paid" : "In progress" })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsx(Text, { size: "xs", color: "muted", children: "Token collected" }), _jsx(Text, { size: "xs", weight: "medium", children: toCurrency(order.emiTokenAmount ?? 0) })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-3", children: [_jsx(Text, { size: "xs", color: "muted", children: "Remaining balance" }), _jsx(Text, { size: "xs", weight: "medium", children: toCurrency(order.emiRemainingBalance ?? 0) })] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.emiInstallments ?? []).map((inst) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { className: "min-w-0", children: [_jsxs(Text, { size: "sm", weight: "medium", children: ["Installment ", inst.index, " \u00B7 ", toCurrency(inst.amount)] }), _jsx(Text, { size: "xs", color: "muted", children: inst.status === "paid" && inst.paidAt
|
|
209
252
|
? `Paid ${toRelativeDate(inst.paidAt)}`
|
|
210
253
|
: inst.dueDate
|
|
211
254
|
? `Due ${toRelativeDate(inst.dueDate)}`
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Badge, Button, Checkbox, Div, Grid, Row, Stack, Text, TextLink } from "../../../ui";
|
|
4
4
|
import { MediaImage } from "../../media/MediaImage";
|
|
5
5
|
import { ROW_ACTION_META, ROW_ACTION_ID } from "../../../features/products/constants/action-defs";
|
|
6
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
6
7
|
import { listingBadgeVariant } from "../../products/utils/listing-badge-variant";
|
|
7
8
|
const __P = {
|
|
8
9
|
p3: "p-[var(--appkit-space-3)]",
|
|
@@ -17,6 +18,27 @@ function buildHref(row) {
|
|
|
17
18
|
return `/pre-orders/${row.id}`;
|
|
18
19
|
return `/products/${row.id}`;
|
|
19
20
|
}
|
|
21
|
+
/*
|
|
22
|
+
* ๐ THE DELETE BUTTONS BELOW CARRY `action`, AND THAT IS WHAT CONFIRMS THEM.
|
|
23
|
+
*
|
|
24
|
+
* They took their LABEL from `ROW_ACTION_META[ROW_ACTION_ID.DELETE]` and
|
|
25
|
+
* passed no `action` prop, so `<Button>`'s confirmation gate โ which fires
|
|
26
|
+
* only when `action.confirmation` is set โ never engaged. One click on a
|
|
27
|
+
* row's Delete permanently removed a PUBLISHED listing with no dialog of any
|
|
28
|
+
* kind; measured on a real seller account, the row was gone immediately and
|
|
29
|
+
* still gone after a reload.
|
|
30
|
+
*
|
|
31
|
+
* Using the registry for the label while bypassing it for the confirmation is
|
|
32
|
+
* exactly the split Rule #7 describes, on the one action where that rule says
|
|
33
|
+
* the cost is data loss. `ACTIONS.STORE["delete-listing"]` already carried the
|
|
34
|
+
* copy ("Delete listing? / This listing will be permanently removed. This
|
|
35
|
+
* action cannot be undone.") and the bulk bar already resolved it correctly;
|
|
36
|
+
* only the row path did not.
|
|
37
|
+
*
|
|
38
|
+
* `variant="ghost"` stays explicit so the row keeps its quiet styling: Button
|
|
39
|
+
* resolves the variant as `variant ?? kindDefault`, so the caller wins and the
|
|
40
|
+
* danger kind drives the dialog without turning the row button red.
|
|
41
|
+
*/
|
|
20
42
|
export function SellerProductsCards({ view, rows, isLoading, listingKind, selectedIds, toggle, onEdit, onDuplicate, onDelete, }) {
|
|
21
43
|
const Container = view === "grid" ? Grid : Stack;
|
|
22
44
|
const containerProps = view === "grid"
|
|
@@ -25,6 +47,6 @@ export function SellerProductsCards({ view, rows, isLoading, listingKind, select
|
|
|
25
47
|
return (_jsxs(Container, { ...containerProps, children: [rows.length === 0 && !isLoading && (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)] col-span-full", size: "sm", children: listingKind !== "all" ? `No ${listingKind} listings found` : "No products listed yet" })), rows.map((row) => {
|
|
26
48
|
const isSelected = selectedIds.has(row.id);
|
|
27
49
|
const href = buildHref(row);
|
|
28
|
-
return view === "grid" ? (_jsxs(Div, { className: "group relative bg-[var(--appkit-color-surface)] overflow-hidden", shadow: "hover-md", rounded: "xl", border: isSelected ? "strong" : "default", children: [_jsx(Div, { className: "absolute top-2 left-2 z-10", children: _jsx(Checkbox, { bare: true, checked: isSelected, onChange: () => toggle(row.id), className: "h-4 w-4 rounded border-[var(--appkit-color-border)]", onClick: (e) => e.stopPropagation(), "aria-label": "Select" }) }), _jsxs(TextLink, { href: href, className: "block", children: [_jsx(Div, { className: "w-full aspect-square", children: _jsx(MediaImage, { src: row.imageUrl, alt: row.primary, size: "card" }) }), _jsxs(Stack, { className: `${__P.p3}`, gap: "xs", children: [_jsx(Text, { className: "line-clamp-1", size: "sm", weight: "medium", children: row.primary }), _jsxs(Row, { gap: "sm", children: [_jsx(Badge, { variant: listingBadgeVariant(row.listingKind), children: row.listingKind }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] line-clamp-1", size: "xs", children: row.secondary })] })] })] }), _jsxs(Row, { className: "border-t border-[var(--appkit-color-border)]", justify: "end", gap: "xs", padding: "xs", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); onEdit(row); }, "aria-label": "Edit", children: ROW_ACTION_META[ROW_ACTION_ID.EDIT].label }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); onDuplicate(row); }, "aria-label": "Duplicate", children: ROW_ACTION_META[ROW_ACTION_ID.DUPLICATE].label }), onDelete && (_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); onDelete(row); }, "aria-label": "Delete", children: ROW_ACTION_META[ROW_ACTION_ID.DELETE].label }))] })] }, row.id)) : (_jsxs(Row, { className: "bg-[var(--appkit-color-surface)] hover:bg-[var(--appkit-color-surface-raised)]", align: "center", gap: "3", rounded: "lg", padding: "inlineSm", border: isSelected ? "strong" : "default", children: [_jsx(Checkbox, { bare: true, checked: isSelected, onChange: () => toggle(row.id), className: "h-4 w-4 rounded border-[var(--appkit-color-border)]", "aria-label": "Select" }), _jsx(Div, { className: `w-12 h-12 ${__O.hidden}`, rounded: "default", children: _jsx(MediaImage, { src: row.imageUrl, alt: row.primary, size: "thumbnail" }) }), _jsxs(TextLink, { href: href, className: "flex-1 min-w-0", children: [_jsx(Text, { className: "line-clamp-1", size: "sm", weight: "medium", children: row.primary }), _jsxs(Row, { gap: "sm", children: [_jsx(Badge, { variant: listingBadgeVariant(row.listingKind), children: row.listingKind }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] line-clamp-1", size: "xs", children: row.secondary })] })] }), _jsxs(Row, { className: "flex-shrink-0", gap: "xs", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => onEdit(row), "aria-label": "Edit", children: ROW_ACTION_META[ROW_ACTION_ID.EDIT].label }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => onDuplicate(row), "aria-label": "Duplicate", children: ROW_ACTION_META[ROW_ACTION_ID.DUPLICATE].label }), onDelete && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => onDelete(row), "aria-label": "Delete", children: ROW_ACTION_META[ROW_ACTION_ID.DELETE].label }))] })] }, row.id));
|
|
50
|
+
return view === "grid" ? (_jsxs(Div, { className: "group relative bg-[var(--appkit-color-surface)] overflow-hidden", shadow: "hover-md", rounded: "xl", border: isSelected ? "strong" : "default", children: [_jsx(Div, { className: "absolute top-2 left-2 z-10", children: _jsx(Checkbox, { bare: true, checked: isSelected, onChange: () => toggle(row.id), className: "h-4 w-4 rounded border-[var(--appkit-color-border)]", onClick: (e) => e.stopPropagation(), "aria-label": "Select" }) }), _jsxs(TextLink, { href: href, className: "block", children: [_jsx(Div, { className: "w-full aspect-square", children: _jsx(MediaImage, { src: row.imageUrl, alt: row.primary, size: "card" }) }), _jsxs(Stack, { className: `${__P.p3}`, gap: "xs", children: [_jsx(Text, { className: "line-clamp-1", size: "sm", weight: "medium", children: row.primary }), _jsxs(Row, { gap: "sm", children: [_jsx(Badge, { variant: listingBadgeVariant(row.listingKind), children: row.listingKind }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] line-clamp-1", size: "xs", children: row.secondary })] })] })] }), _jsxs(Row, { className: "border-t border-[var(--appkit-color-border)]", justify: "end", gap: "xs", padding: "xs", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); onEdit(row); }, "aria-label": "Edit", children: ROW_ACTION_META[ROW_ACTION_ID.EDIT].label }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); onDuplicate(row); }, "aria-label": "Duplicate", children: ROW_ACTION_META[ROW_ACTION_ID.DUPLICATE].label }), onDelete && (_jsx(Button, { variant: "ghost", size: "sm", action: ACTIONS.STORE["delete-listing"], onClick: (e) => { e.stopPropagation(); onDelete(row); }, "aria-label": "Delete", children: ROW_ACTION_META[ROW_ACTION_ID.DELETE].label }))] })] }, row.id)) : (_jsxs(Row, { className: "bg-[var(--appkit-color-surface)] hover:bg-[var(--appkit-color-surface-raised)]", align: "center", gap: "3", rounded: "lg", padding: "inlineSm", border: isSelected ? "strong" : "default", children: [_jsx(Checkbox, { bare: true, checked: isSelected, onChange: () => toggle(row.id), className: "h-4 w-4 rounded border-[var(--appkit-color-border)]", "aria-label": "Select" }), _jsx(Div, { className: `w-12 h-12 ${__O.hidden}`, rounded: "default", children: _jsx(MediaImage, { src: row.imageUrl, alt: row.primary, size: "thumbnail" }) }), _jsxs(TextLink, { href: href, className: "flex-1 min-w-0", children: [_jsx(Text, { className: "line-clamp-1", size: "sm", weight: "medium", children: row.primary }), _jsxs(Row, { gap: "sm", children: [_jsx(Badge, { variant: listingBadgeVariant(row.listingKind), children: row.listingKind }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] line-clamp-1", size: "xs", children: row.secondary })] })] }), _jsxs(Row, { className: "flex-shrink-0", gap: "xs", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => onEdit(row), "aria-label": "Edit", children: ROW_ACTION_META[ROW_ACTION_ID.EDIT].label }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => onDuplicate(row), "aria-label": "Duplicate", children: ROW_ACTION_META[ROW_ACTION_ID.DUPLICATE].label }), onDelete && (_jsx(Button, { variant: "ghost", size: "sm", action: ACTIONS.STORE["delete-listing"], onClick: () => onDelete(row), "aria-label": "Delete", children: ROW_ACTION_META[ROW_ACTION_ID.DELETE].label }))] })] }, row.id));
|
|
29
51
|
})] }));
|
|
30
52
|
}
|
|
@@ -17,7 +17,9 @@ import { SELLER_PRODUCT_STATUS_TABS } from "../../admin/constants/filter-tabs";
|
|
|
17
17
|
import { ROUTES } from "../../../constants";
|
|
18
18
|
import { normalizeListingType } from "../../products/utils/listing-type";
|
|
19
19
|
import { useAvailabilityScope } from "../../products/hooks/useAvailabilityScope";
|
|
20
|
-
import { toRecordArray,
|
|
20
|
+
import { toRecordArray, toStringValue } from "../hooks/useSellerListingData";
|
|
21
|
+
// Defining module (Root Cause #18) โ the seller-side duplicate was deleted for drift.
|
|
22
|
+
import { toRelativeDate } from "../../admin/hooks/useAdminListingData";
|
|
21
23
|
import { useListingTypeFlags } from "../../../react/hooks/useListingTypeFlags";
|
|
22
24
|
import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags";
|
|
23
25
|
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
@@ -267,17 +269,40 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
|
|
|
267
269
|
* the UI simply never presented it.
|
|
268
270
|
*/
|
|
269
271
|
const statusLabel = toStringValue(item.status, "draft");
|
|
272
|
+
/*
|
|
273
|
+
* ๐ PRICE AND STOCK GO IN THE VISIBLE LINE TOO โ same reason as the
|
|
274
|
+
* status note above, one field over.
|
|
275
|
+
*
|
|
276
|
+
* `price` was already mapped onto the row and correct, and this view
|
|
277
|
+
* renders only `primary`, `secondary`, the type chip and the action
|
|
278
|
+
* buttons โ so it was never displayed. Measured on a real seller
|
|
279
|
+
* account with 127 listings: ZERO rupee amounts and zero stock figures
|
|
280
|
+
* anywhere in the list. To learn what one of their own listings costs
|
|
281
|
+
* or how many are left, a seller had to open it.
|
|
282
|
+
*
|
|
283
|
+
* Those are the two numbers a seller scans for, so price leads.
|
|
284
|
+
* `stockQuantity` was not mapped at all and is added here.
|
|
285
|
+
*
|
|
286
|
+
* An auction keeps its own summary (reserve / bids / ends) and is not
|
|
287
|
+
* given a list price: for an auction the meaningful money is the
|
|
288
|
+
* reserve and the current bid, both already in `auctionSecondary`, and
|
|
289
|
+
* a "price" beside them would read as a third, competing figure.
|
|
290
|
+
*/
|
|
291
|
+
const stockRaw = typeof item.stockQuantity === "number" ? item.stockQuantity : null;
|
|
292
|
+
const priceLabel = kind !== "auction" && priceRaw ? `โน${priceRaw.toLocaleString("en-IN")}` : "";
|
|
293
|
+
const stockLabel = stockRaw === null ? "" : `${stockRaw} in stock`;
|
|
270
294
|
const secondaryBase = auctionSecondary || toStringValue(item.condition, "");
|
|
271
295
|
return {
|
|
272
296
|
id: toStringValue(item.id, `product-${index}`),
|
|
273
297
|
primary: toStringValue(item.title ?? item.name, "Untitled product"),
|
|
274
|
-
secondary: [secondaryBase, statusLabel].filter(Boolean).join(" ยท "),
|
|
298
|
+
secondary: [priceLabel, stockLabel, secondaryBase, statusLabel].filter(Boolean).join(" ยท "),
|
|
275
299
|
status: statusLabel,
|
|
276
300
|
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
277
301
|
imageUrl: toStringValue(item.mainImage ?? item.images?.[0], undefined),
|
|
278
302
|
image: toStringValue(item.mainImage ?? item.images?.[0], undefined),
|
|
279
303
|
listingKind: kind,
|
|
280
304
|
price: priceRaw ? `โน${priceRaw.toLocaleString("en-IN")}` : "โ",
|
|
305
|
+
stock: stockLabel || "โ",
|
|
281
306
|
physicalLocation: item.physicalLocation &&
|
|
282
307
|
typeof item.physicalLocation.zone === "string"
|
|
283
308
|
? item.physicalLocation
|
|
@@ -14,5 +14,4 @@ type UnknownRecord = Record<string, JsonValue>;
|
|
|
14
14
|
export declare function toRecordArray(value: unknown): UnknownRecord[];
|
|
15
15
|
export declare function toStringValue(value: unknown, fallback?: string): string;
|
|
16
16
|
export declare function toCurrency(value: unknown): string;
|
|
17
|
-
export declare function toRelativeDate(value: unknown): string;
|
|
18
17
|
export {};
|
|
@@ -18,25 +18,23 @@ export function toCurrency(value) {
|
|
|
18
18
|
}
|
|
19
19
|
return formatCurrency(value);
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return date.toLocaleDateString("en-IN", { year: "numeric", month: "short", day: "numeric" });
|
|
39
|
-
}
|
|
21
|
+
/*
|
|
22
|
+
* ๐ `toRelativeDate` WAS HERE AND IS GONE. Import it from
|
|
23
|
+
* `features/admin/hooks/useAdminListingData` โ the defining module now, and
|
|
24
|
+
* the one 44 files already use (Root Cause #18: import from where a symbol is
|
|
25
|
+
* defined, never from a convenience copy).
|
|
26
|
+
*
|
|
27
|
+
* This copy had drifted from that one and the drift was live: for a FUTURE
|
|
28
|
+
* date, `deltaMs` is negative, so `deltaMs < minute` matched and this version
|
|
29
|
+
* answered **"just now"** while the admin version answered **"1m ago"** โ two
|
|
30
|
+
* different wrong answers to "when is this deadline", depending on which file
|
|
31
|
+
* a view happened to import from. The admin one is fixed to say "in 48h"; a
|
|
32
|
+
* second implementation is how that fix would have been half-applied.
|
|
33
|
+
*
|
|
34
|
+
* The three sibling helpers below are also duplicated there. They are left
|
|
35
|
+
* alone deliberately: none of them is wrong, and collapsing them is an import
|
|
36
|
+
* rewrite across both feature trees rather than a bug fix.
|
|
37
|
+
*/
|
|
40
38
|
function parseDate(value) {
|
|
41
39
|
if (value instanceof Date)
|
|
42
40
|
return value;
|