@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.
Files changed (181) hide show
  1. package/dist/_internal/server/features/offers/adapters.d.ts +29 -7
  2. package/dist/_internal/server/features/offers/adapters.js +14 -3
  3. package/dist/_internal/server/features/orders/adapters.js +54 -1
  4. package/dist/client.d.ts +1 -0
  5. package/dist/client.js +1 -0
  6. package/dist/features/account/components/UserOrdersView.js +30 -2
  7. package/dist/features/account/hooks/useAddresses.d.ts +21 -6
  8. package/dist/features/account/hooks/useAddresses.js +1 -1
  9. package/dist/features/addresses/hooks/usePostalLookup.js +84 -12
  10. package/dist/features/admin/components/AdminCategoriesView.js +37 -2
  11. package/dist/features/admin/components/AdminOffersView.js +2 -2
  12. package/dist/features/admin/constants/filter-tabs.d.ts +25 -0
  13. package/dist/features/admin/constants/filter-tabs.js +30 -0
  14. package/dist/features/admin/hooks/useAdminListingData.d.ts +35 -0
  15. package/dist/features/admin/hooks/useAdminListingData.js +45 -9
  16. package/dist/features/cart/schemas/index.d.ts +15 -0
  17. package/dist/features/cart/schemas/index.js +2 -0
  18. package/dist/features/cart/types/index.d.ts +22 -0
  19. package/dist/features/categories/components/CategoriesIndexListing.js +19 -1
  20. package/dist/features/categories/components/CategoriesIndexPageView.js +14 -1
  21. package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +8 -0
  22. package/dist/features/classified/components/ClassifiedDetailPageView.js +1 -0
  23. package/dist/features/events/actions/event-actions.d.ts +13 -0
  24. package/dist/features/events/actions/event-actions.js +75 -1
  25. package/dist/features/events/repository/event-entry.repository.d.ts +17 -0
  26. package/dist/features/events/repository/event-entry.repository.js +36 -0
  27. package/dist/features/homepage/components/BrandsSection.js +1 -1
  28. package/dist/features/homepage/components/CustomerReviewsSection.js +1 -1
  29. package/dist/features/homepage/components/FeaturedBundlesSection.js +1 -1
  30. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  31. package/dist/features/media/upload/MediaUploadField.js +21 -3
  32. package/dist/features/orders/components/OrdersList.js +2 -1
  33. package/dist/features/orders/index.d.ts +1 -0
  34. package/dist/features/orders/index.js +1 -0
  35. package/dist/features/orders/types/index.d.ts +2 -0
  36. package/dist/features/orders/utils/order-ref.d.ts +45 -0
  37. package/dist/features/orders/utils/order-ref.js +58 -0
  38. package/dist/features/products/components/MakeOfferButton.d.ts +18 -1
  39. package/dist/features/products/components/MakeOfferButton.js +23 -1
  40. package/dist/features/products/components/ProductDetailPageView.d.ts +5 -0
  41. package/dist/features/products/components/ProductDetailPageView.js +3 -2
  42. package/dist/features/products/repository/products.repository.js +29 -2
  43. package/dist/features/seller/actions/offer-actions.js +32 -0
  44. package/dist/features/seller/components/QuickProductForm.js +15 -1
  45. package/dist/features/seller/components/SellerBidsView.js +3 -1
  46. package/dist/features/seller/components/SellerOffersView.js +2 -2
  47. package/dist/features/seller/components/SellerOrdersView.js +49 -6
  48. package/dist/features/seller/components/SellerProductsCards.js +23 -1
  49. package/dist/features/seller/components/SellerProductsView.js +27 -2
  50. package/dist/features/seller/hooks/useSellerListingData.d.ts +0 -1
  51. package/dist/features/seller/hooks/useSellerListingData.js +17 -19
  52. package/dist/features/shell/SectionForm.js +33 -2
  53. package/dist/features/tester/seed-data/_happy-path.d.ts +23 -0
  54. package/dist/features/tester/seed-data/_happy-path.js +710 -0
  55. package/dist/features/tester/seed-data/_money-flows.js +34 -23
  56. package/dist/features/tester/seed-data/authored/account-auth__profile-settings.js +22 -3
  57. package/dist/features/tester/seed-data/authored/addresses__address-filters.js +17 -12
  58. package/dist/features/tester/seed-data/authored/addresses__postal-lookup.js +7 -6
  59. package/dist/features/tester/seed-data/authored/addresses__postal-validation.js +10 -4
  60. package/dist/features/tester/seed-data/authored/addresses__state-picker.js +13 -8
  61. package/dist/features/tester/seed-data/authored/addresses__unban-request.js +49 -11
  62. package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +10 -6
  63. package/dist/features/tester/seed-data/authored/admin__bans-and-trust.js +71 -32
  64. package/dist/features/tester/seed-data/authored/admin__blog-faqs.js +80 -31
  65. package/dist/features/tester/seed-data/authored/admin__bug-hunter-rewards.js +62 -25
  66. package/dist/features/tester/seed-data/authored/admin__bulk-actions.js +61 -31
  67. package/dist/features/tester/seed-data/authored/admin__bundles.js +84 -37
  68. package/dist/features/tester/seed-data/authored/admin__buyer-data-admin.js +90 -57
  69. package/dist/features/tester/seed-data/authored/admin__catalog-listings.js +38 -26
  70. package/dist/features/tester/seed-data/authored/admin__category-brand-authoring.js +47 -28
  71. package/dist/features/tester/seed-data/authored/admin__classifieds-digitalcodes-live.js +118 -31
  72. package/dist/features/tester/seed-data/authored/admin__content-deletes.js +30 -24
  73. package/dist/features/tester/seed-data/authored/admin__content-marketing.js +83 -46
  74. package/dist/features/tester/seed-data/authored/admin__coupons.js +8 -8
  75. package/dist/features/tester/seed-data/authored/admin__events-raffles-spin.js +9 -9
  76. package/dist/features/tester/seed-data/authored/admin__firebase-function-effects.js +19 -4
  77. package/dist/features/tester/seed-data/authored/admin__media-watermark.js +77 -33
  78. package/dist/features/tester/seed-data/authored/admin__orders-fulfillment.js +5 -5
  79. package/dist/features/tester/seed-data/authored/admin__prize-draws-lotteries.js +29 -5
  80. package/dist/features/tester/seed-data/authored/admin__site-system.js +45 -23
  81. package/dist/features/tester/seed-data/authored/admin__uncovered-admin-pages.js +53 -28
  82. package/dist/features/tester/seed-data/authored/admin__users-trust.js +20 -20
  83. package/dist/features/tester/seed-data/authored/buying__bidding.js +19 -2
  84. package/dist/features/tester/seed-data/authored/buying__browsing-search.js +20 -5
  85. package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +28 -10
  86. package/dist/features/tester/seed-data/authored/buying__cart.js +10 -3
  87. package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +0 -16
  88. package/dist/features/tester/seed-data/authored/buying__my-orders.js +27 -5
  89. package/dist/features/tester/seed-data/authored/buying__offers.js +22 -7
  90. package/dist/features/tester/seed-data/authored/buying__order-detail-actions.js +3 -3
  91. package/dist/features/tester/seed-data/authored/buying__product-detail.js +35 -4
  92. package/dist/features/tester/seed-data/authored/buying__return-request.js +19 -6
  93. package/dist/features/tester/seed-data/authored/buying__reviews-pagination.js +36 -18
  94. package/dist/features/tester/seed-data/authored/buying__reviews.js +16 -1
  95. package/dist/features/tester/seed-data/authored/buying__user-dashboard-navigation.js +2 -1
  96. package/dist/features/tester/seed-data/authored/buying__user-uncovered-pages.js +16 -1
  97. package/dist/features/tester/seed-data/authored/community-support__public-profile.js +3 -2
  98. package/dist/features/tester/seed-data/authored/community-support__support-tickets.js +4 -4
  99. package/dist/features/tester/seed-data/authored/content-discovery__blog.js +19 -2
  100. package/dist/features/tester/seed-data/authored/content-discovery__category-brand-relations.js +4 -4
  101. package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +5 -5
  102. package/dist/features/tester/seed-data/authored/content-discovery__coupons.js +41 -6
  103. package/dist/features/tester/seed-data/authored/content-discovery__event-detail-subroutes.js +63 -8
  104. package/dist/features/tester/seed-data/authored/content-discovery__event-participation.js +29 -29
  105. package/dist/features/tester/seed-data/authored/content-discovery__events.js +2 -2
  106. package/dist/features/tester/seed-data/authored/content-discovery__faq-help.js +23 -1
  107. package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +6 -6
  108. package/dist/features/tester/seed-data/authored/content-discovery__search.js +1 -1
  109. package/dist/features/tester/seed-data/authored/content-discovery__store-reviews-aggregate.js +1 -1
  110. package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.js +31 -6
  111. package/dist/features/tester/seed-data/authored/cta-layout__dialog-footers.js +34 -10
  112. package/dist/features/tester/seed-data/authored/cta-layout__editor-action-bar.js +35 -8
  113. package/dist/features/tester/seed-data/authored/cta-layout__navbar-ctas.js +31 -9
  114. package/dist/features/tester/seed-data/authored/cta-layout__product-bottom-bar.js +29 -3
  115. package/dist/features/tester/seed-data/authored/design-ux__back-to-top-button.js +1 -1
  116. package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.js +10 -10
  117. package/dist/features/tester/seed-data/authored/design-ux__dashboard-layout.js +3 -3
  118. package/dist/features/tester/seed-data/authored/design-ux__footer-theme.js +2 -2
  119. package/dist/features/tester/seed-data/authored/design-ux__form-validation-errors.js +2 -2
  120. package/dist/features/tester/seed-data/authored/design-ux__general-design.js +4 -4
  121. package/dist/features/tester/seed-data/authored/design-ux__hand-mode-layout.js +1 -1
  122. package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.js +8 -8
  123. package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +2 -2
  124. package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +2 -2
  125. package/dist/features/tester/seed-data/authored/page-wiring__data-loss.js +39 -16
  126. package/dist/features/tester/seed-data/authored/page-wiring__detail-pages.js +43 -8
  127. package/dist/features/tester/seed-data/authored/page-wiring__drawer-pages.js +40 -14
  128. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +31 -14
  129. package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +7 -6
  130. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +14 -5
  131. package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +3 -3
  132. package/dist/features/tester/seed-data/authored/public-pages__help-how-it-works.js +1 -1
  133. package/dist/features/tester/seed-data/authored/public-pages__help-scams-guides-subpages.js +4 -2
  134. package/dist/features/tester/seed-data/authored/public-pages__legal-policy-pages.js +3 -3
  135. package/dist/features/tester/seed-data/authored/public-pages__newly-wired-browse-indexes.js +5 -5
  136. package/dist/features/tester/seed-data/authored/public-pages__store-tabs.js +5 -5
  137. package/dist/features/tester/seed-data/authored/public-pages__stores-sellers-directories.js +3 -3
  138. package/dist/features/tester/seed-data/authored/search-and-nav__employee-permissions.js +45 -11
  139. package/dist/features/tester/seed-data/authored/search-and-nav__header-search.js +57 -17
  140. package/dist/features/tester/seed-data/authored/search-and-nav__settings-deep-links.js +36 -5
  141. package/dist/features/tester/seed-data/authored/search-and-nav__sidebar-search.js +29 -4
  142. package/dist/features/tester/seed-data/authored/selling__become-seller.js +23 -0
  143. package/dist/features/tester/seed-data/authored/selling__final-sale-authoring.js +14 -13
  144. package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +14 -9
  145. package/dist/features/tester/seed-data/authored/selling__listing-edit-roundtrip.js +34 -7
  146. package/dist/features/tester/seed-data/authored/selling__listing-lifecycle.js +17 -7
  147. package/dist/features/tester/seed-data/authored/selling__listing-type-fields-roundtrip.js +9 -9
  148. package/dist/features/tester/seed-data/authored/selling__media-limits.js +9 -4
  149. package/dist/features/tester/seed-data/authored/selling__product-upload-details.js +3 -3
  150. package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.js +3 -3
  151. package/dist/features/tester/seed-data/authored/selling__sectionised-forms.js +25 -1
  152. package/dist/features/tester/seed-data/authored/selling__seller-analytics-payouts.js +27 -6
  153. package/dist/features/tester/seed-data/authored/selling__seller-bids-bundles-filters.js +19 -1
  154. package/dist/features/tester/seed-data/authored/selling__seller-catalog-org.js +63 -22
  155. package/dist/features/tester/seed-data/authored/selling__seller-custom-brands.js +9 -5
  156. package/dist/features/tester/seed-data/authored/selling__seller-guide.js +7 -6
  157. package/dist/features/tester/seed-data/authored/selling__seller-listing-types.js +3 -2
  158. package/dist/features/tester/seed-data/authored/selling__seller-marketing-extras.js +18 -18
  159. package/dist/features/tester/seed-data/authored/selling__seller-ops-comms.js +13 -12
  160. package/dist/features/tester/seed-data/authored/selling__seller-orders-at-scale.js +19 -3
  161. package/dist/features/tester/seed-data/authored/selling__seller-orders.js +22 -4
  162. package/dist/features/tester/seed-data/authored/selling__seller-shipping-payouts-setup.js +20 -8
  163. package/dist/features/tester/seed-data/authored/selling__store-dashboard-navigation.js +4 -4
  164. package/dist/features/tester/seed-data/authored/selling__store-uncovered-pages.js +2 -2
  165. package/dist/features/tester/seed-data/authored/seo__canonical-and-host.js +5 -5
  166. package/dist/features/tester/seed-data/authored/seo__og-images.js +1 -1
  167. package/dist/features/tester/seed-data/authored/seo__page-metadata.js +7 -7
  168. package/dist/features/tester/seed-data/authored/seo__sitemap-and-robots.js +3 -3
  169. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +123 -21
  170. package/dist/features/tester/utils/phases.d.ts +17 -0
  171. package/dist/features/tester/utils/phases.js +29 -2
  172. package/dist/index.d.ts +1 -0
  173. package/dist/index.js +2 -0
  174. package/dist/seo/json-ld.d.ts +23 -0
  175. package/dist/seo/json-ld.js +3 -2
  176. package/dist/server.d.ts +1 -0
  177. package/dist/server.js +2 -0
  178. package/dist/styles.css +1 -1
  179. package/dist/tailwind-utilities.css +1 -1
  180. package/dist/ui/components/PaginatedSelect.js +0 -0
  181. 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
- const leaf = (Array.isArray(input.categorySlugs) && input.categorySlugs[0]) ||
134
- input.category ||
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: "Brief description (optional)", rows: 3 }), _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)" })] })] }));
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, toRelativeDate, toCurrency, toStringValue } from "../hooks/useSellerListingData";
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, "Pending"),
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, toRelativeDate, toCurrency, toStringValue } from "../hooks/useSellerListingData";
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
- carrier: o.carrier ?? "",
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
- carrier: next.carrier ?? "",
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
- const addr = order?.shippingAddress ?? {};
207
- const addrLine = [addr.addressLine1, addr.city, addr.state, addr.pincode].filter(Boolean).join(", ");
208
- 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.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.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 ?? 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
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, toRelativeDate, toStringValue } from "../hooks/useSellerListingData";
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
- export function toRelativeDate(value) {
22
- const date = parseDate(value);
23
- if (!date) {
24
- return "-";
25
- }
26
- const deltaMs = Date.now() - date.getTime();
27
- const minute = 60000;
28
- const hour = 60 * minute;
29
- const day = 24 * hour;
30
- if (deltaMs < minute)
31
- return "just now";
32
- if (deltaMs < hour)
33
- return `${Math.floor(deltaMs / minute)}m ago`;
34
- if (deltaMs < day)
35
- return `${Math.floor(deltaMs / hour)}h ago`;
36
- if (deltaMs < 7 * day)
37
- return `${Math.floor(deltaMs / day)}d ago`;
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;