@mohasinac/appkit 4.11.2 → 4.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/dist/_internal/server/features/checkout/actions.d.ts +40 -3
  2. package/dist/_internal/server/features/checkout/actions.js +232 -47
  3. package/dist/_internal/server/features/checkout/data.d.ts +10 -0
  4. package/dist/_internal/server/features/orders/adapters.js +20 -0
  5. package/dist/_internal/server/features/products/data.d.ts +12 -3
  6. package/dist/_internal/server/features/products/data.js +55 -4
  7. package/dist/_internal/server/features/products/index.d.ts +1 -1
  8. package/dist/_internal/server/features/products/index.js +1 -1
  9. package/dist/_internal/server/features/products/list-public.d.ts +47 -0
  10. package/dist/_internal/server/features/products/list-public.js +131 -6
  11. package/dist/_internal/server/features/reviews/data.d.ts +1 -1
  12. package/dist/_internal/server/features/reviews/data.js +3 -2
  13. package/dist/_internal/server/features/tester/visibility.js +3 -3
  14. package/dist/_internal/server/jobs/core/auctionSettlement.js +5 -2
  15. package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -1
  16. package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
  17. package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +2 -0
  18. package/dist/_internal/server/jobs/handlers/_helpers.js +4 -1
  19. package/dist/_internal/shared/checkout/lanes.d.ts +20 -0
  20. package/dist/_internal/shared/checkout/lanes.js +28 -0
  21. package/dist/_internal/shared/features/checkout/config.d.ts +4 -0
  22. package/dist/_internal/shared/features/checkout/config.js +8 -0
  23. package/dist/_internal/shared/features/promotions/schema.d.ts +0 -16
  24. package/dist/_internal/shared/features/promotions/schema.js +0 -2
  25. package/dist/_internal/shared/features/reviews/config.d.ts +8 -1
  26. package/dist/_internal/shared/features/reviews/config.js +8 -1
  27. package/dist/_internal/shared/fees/calculator.d.ts +34 -2
  28. package/dist/_internal/shared/fees/calculator.js +45 -1
  29. package/dist/_internal/shared/listing-types/_registry.d.ts +88 -1
  30. package/dist/_internal/shared/listing-types/_registry.js +63 -1
  31. package/dist/_internal/shared/listing-types/art/config.d.ts +59 -0
  32. package/dist/_internal/shared/listing-types/art/config.js +12 -0
  33. package/dist/_internal/shared/listing-types/auction/config.d.ts +53 -0
  34. package/dist/_internal/shared/listing-types/auction/config.js +17 -0
  35. package/dist/_internal/shared/listing-types/classified/config.d.ts +59 -0
  36. package/dist/_internal/shared/listing-types/classified/config.js +14 -0
  37. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +59 -0
  38. package/dist/_internal/shared/listing-types/digital-code/config.js +12 -0
  39. package/dist/_internal/shared/listing-types/live/config.d.ts +59 -0
  40. package/dist/_internal/shared/listing-types/live/config.js +14 -0
  41. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +110 -0
  42. package/dist/_internal/shared/listing-types/pre-order/config.js +14 -0
  43. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +44 -0
  44. package/dist/_internal/shared/listing-types/prize-draw/config.js +10 -0
  45. package/dist/_internal/shared/listing-types/standard/config.d.ts +59 -0
  46. package/dist/_internal/shared/listing-types/standard/config.js +9 -0
  47. package/dist/_internal/shared/listing-types/stickers/config.d.ts +59 -0
  48. package/dist/_internal/shared/listing-types/stickers/config.js +12 -0
  49. package/dist/client.d.ts +12 -2
  50. package/dist/client.js +8 -2
  51. package/dist/constants/api-endpoints.d.ts +6 -0
  52. package/dist/constants/api-endpoints.js +2 -0
  53. package/dist/constants/field-names.d.ts +12 -0
  54. package/dist/constants/field-names.js +12 -0
  55. package/dist/constants/table-keys.d.ts +2 -0
  56. package/dist/constants/table-keys.js +2 -0
  57. package/dist/features/account/components/UserOrdersView.js +27 -5
  58. package/dist/features/admin/components/AdminAddressesView.js +21 -11
  59. package/dist/features/admin/components/AdminArtView.d.ts +9 -0
  60. package/dist/features/admin/components/AdminArtView.js +11 -35
  61. package/dist/features/admin/components/AdminClassifiedView.d.ts +9 -0
  62. package/dist/features/admin/components/AdminClassifiedView.js +11 -35
  63. package/dist/features/admin/components/AdminCouponEditorView.js +1 -4
  64. package/dist/features/admin/components/AdminDigitalCodesView.d.ts +9 -0
  65. package/dist/features/admin/components/AdminDigitalCodesView.js +11 -42
  66. package/dist/features/admin/components/AdminLiveView.d.ts +9 -0
  67. package/dist/features/admin/components/AdminLiveView.js +11 -41
  68. package/dist/features/admin/components/AdminOrderEditorView.d.ts +8 -1
  69. package/dist/features/admin/components/AdminOrderEditorView.js +4 -3
  70. package/dist/features/admin/components/AdminOrdersView.js +14 -1
  71. package/dist/features/admin/components/AdminPaymentMethodsView.js +18 -11
  72. package/dist/features/admin/components/AdminProductsView.js +34 -18
  73. package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
  74. package/dist/features/admin/components/AdminSiteSettingsView.js +4 -2
  75. package/dist/features/admin/components/AdminStickersView.d.ts +9 -0
  76. package/dist/features/admin/components/AdminStickersView.js +11 -35
  77. package/dist/features/admin/constants/filter-tabs.d.ts +26 -9
  78. package/dist/features/admin/constants/filter-tabs.js +22 -9
  79. package/dist/features/admin/hooks/useAdminListing.js +7 -2
  80. package/dist/features/admin/schemas/firestore.d.ts +5 -0
  81. package/dist/features/admin/schemas/firestore.js +1 -0
  82. package/dist/features/auctions/actions/bid-actions.js +2 -2
  83. package/dist/features/auctions/components/AuctionBidsTable.js +1 -1
  84. package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
  85. package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
  86. package/dist/features/cart/components/CartDrawer.d.ts +9 -1
  87. package/dist/features/cart/components/CartDrawer.js +4 -3
  88. package/dist/features/cart/components/CartPriceBreakdown.d.ts +61 -0
  89. package/dist/features/cart/components/CartPriceBreakdown.js +25 -0
  90. package/dist/features/cart/components/StoreAddonsPicker.d.ts +47 -0
  91. package/dist/features/cart/components/StoreAddonsPicker.js +21 -0
  92. package/dist/features/cart/components/index.d.ts +4 -0
  93. package/dist/features/cart/components/index.js +2 -0
  94. package/dist/features/cart/repository/cart.repository.d.ts +20 -2
  95. package/dist/features/cart/repository/cart.repository.js +49 -0
  96. package/dist/features/cart/schemas/firestore.d.ts +26 -2
  97. package/dist/features/categories/components/BrandDetailTabs.js +31 -11
  98. package/dist/features/categories/components/CategoryDetailTabs.js +27 -7
  99. package/dist/features/categories/components/CategoryProductsListing.d.ts +9 -1
  100. package/dist/features/categories/components/CategoryProductsListing.js +5 -2
  101. package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
  102. package/dist/features/contact/email.js +5 -1
  103. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +17 -16
  104. package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
  105. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  106. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  107. package/dist/features/layout/AppLayoutShell.js +9 -2
  108. package/dist/features/layout/BottomActions.js +62 -6
  109. package/dist/features/layout/BottomActionsContext.d.ts +26 -0
  110. package/dist/features/layout/BottomActionsContext.js +18 -1
  111. package/dist/features/layout/hooks/useBottomActions.d.ts +12 -1
  112. package/dist/features/layout/hooks/useBottomActions.js +14 -1
  113. package/dist/features/layout/index.d.ts +1 -1
  114. package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
  115. package/dist/features/orders/components/OrderAddonBadges.d.ts +36 -0
  116. package/dist/features/orders/components/OrderAddonBadges.js +28 -0
  117. package/dist/features/orders/components/index.d.ts +1 -0
  118. package/dist/features/orders/components/index.js +1 -0
  119. package/dist/features/orders/repository/orders.repository.d.ts +36 -0
  120. package/dist/features/orders/repository/orders.repository.js +18 -0
  121. package/dist/features/orders/types/index.d.ts +38 -0
  122. package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
  123. package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
  124. package/dist/features/products/components/ArtStickersListView.js +5 -4
  125. package/dist/features/products/components/AuctionsIndexListing.js +1 -2
  126. package/dist/features/products/components/ListingBottomActions.d.ts +28 -0
  127. package/dist/features/products/components/ListingBottomActions.js +25 -0
  128. package/dist/features/products/components/PrizeDrawBottomActions.d.ts +1 -1
  129. package/dist/features/products/components/PrizeDrawBottomActions.js +3 -21
  130. package/dist/features/products/components/PrizeDrawsListingView.js +18 -33
  131. package/dist/features/products/components/ProductDetailActions.js +4 -0
  132. package/dist/features/products/components/ProductDetailPageView.js +7 -7
  133. package/dist/features/products/components/ProductsIndexListing.js +87 -11
  134. package/dist/features/products/components/ProductsIndexPageView.js +14 -2
  135. package/dist/features/products/config/listing-type-listing-config.d.ts +31 -0
  136. package/dist/features/products/config/listing-type-listing-config.js +74 -0
  137. package/dist/features/products/constants/listing-tabs.d.ts +43 -198
  138. package/dist/features/products/constants/listing-tabs.js +71 -58
  139. package/dist/features/products/constants/sieve.d.ts +104 -1
  140. package/dist/features/products/constants/sieve.js +27 -7
  141. package/dist/features/products/hooks/useProducts.js +14 -0
  142. package/dist/features/products/repository/products.repository.d.ts +45 -1
  143. package/dist/features/products/repository/products.repository.js +26 -3
  144. package/dist/features/products/types/index.d.ts +15 -0
  145. package/dist/features/products/utils/listing-badge-variant.d.ts +20 -1
  146. package/dist/features/products/utils/listing-badge-variant.js +26 -1
  147. package/dist/features/products/utils/listing-type.d.ts +24 -0
  148. package/dist/features/products/utils/listing-type.js +48 -0
  149. package/dist/features/promotions/actions/coupon-actions.d.ts +7 -0
  150. package/dist/features/promotions/actions/coupon-actions.js +12 -1
  151. package/dist/features/promotions/actions/coupon-stacking.d.ts +24 -0
  152. package/dist/features/promotions/actions/coupon-stacking.js +21 -0
  153. package/dist/features/promotions/actions/index.d.ts +1 -0
  154. package/dist/features/promotions/actions/index.js +1 -0
  155. package/dist/features/promotions/actions/seller-coupon-actions.d.ts +0 -1
  156. package/dist/features/promotions/actions/seller-coupon-actions.js +0 -1
  157. package/dist/features/promotions/components/CouponHelpDetails.d.ts +20 -0
  158. package/dist/features/promotions/components/CouponHelpDetails.js +17 -0
  159. package/dist/features/promotions/components/CouponsIndexListing.js +2 -1
  160. package/dist/features/promotions/components/index.d.ts +2 -0
  161. package/dist/features/promotions/components/index.js +1 -0
  162. package/dist/features/promotions/constants/coupon-help.d.ts +27 -0
  163. package/dist/features/promotions/constants/coupon-help.js +42 -0
  164. package/dist/features/promotions/repository/coupons.repository.d.ts +10 -1
  165. package/dist/features/promotions/repository/coupons.repository.js +22 -8
  166. package/dist/features/promotions/schemas/firestore.d.ts +0 -1
  167. package/dist/features/promotions/schemas/firestore.js +1 -1
  168. package/dist/features/promotions/schemas/index.d.ts +0 -20
  169. package/dist/features/promotions/schemas/index.js +0 -1
  170. package/dist/features/reviews/actions/review-actions.js +5 -1
  171. package/dist/features/reviews/components/ReviewDetailPageView.js +2 -1
  172. package/dist/features/reviews/components/ReviewFilters.d.ts +7 -1
  173. package/dist/features/reviews/components/ReviewFilters.js +4 -3
  174. package/dist/features/reviews/components/ReviewsIndexListing.d.ts +6 -0
  175. package/dist/features/reviews/components/ReviewsIndexListing.js +9 -109
  176. package/dist/features/reviews/components/ReviewsListingPanel.d.ts +41 -0
  177. package/dist/features/reviews/components/ReviewsListingPanel.js +226 -0
  178. package/dist/features/reviews/components/index.d.ts +2 -0
  179. package/dist/features/reviews/components/index.js +1 -0
  180. package/dist/features/reviews/hooks/useReviews.d.ts +8 -0
  181. package/dist/features/reviews/hooks/useReviews.js +14 -6
  182. package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
  183. package/dist/features/reviews/repository/reviews.repository.js +77 -41
  184. package/dist/features/reviews/types/index.d.ts +8 -1
  185. package/dist/features/seller/components/SellerOrdersView.js +2 -1
  186. package/dist/features/seller/components/SellerProductsCards.js +2 -2
  187. package/dist/features/seller/components/SellerProductsView.js +21 -18
  188. package/dist/features/stores/actions/store-query-actions.js +1 -1
  189. package/dist/features/stores/components/StoreArtStickersPageView.d.ts +21 -0
  190. package/dist/features/stores/components/StoreArtStickersPageView.js +30 -0
  191. package/dist/features/stores/components/StoreAuctionsListing.js +5 -11
  192. package/dist/features/stores/components/StoreAuctionsPageView.d.ts +4 -1
  193. package/dist/features/stores/components/StoreAuctionsPageView.js +17 -17
  194. package/dist/features/stores/components/StoreClassifiedsListing.js +7 -0
  195. package/dist/features/stores/components/StoreClassifiedsPageView.d.ts +4 -1
  196. package/dist/features/stores/components/StoreClassifiedsPageView.js +16 -12
  197. package/dist/features/stores/components/StoreDetailLayoutView.js +4 -3
  198. package/dist/features/stores/components/StoreDigitalCodesListing.js +5 -0
  199. package/dist/features/stores/components/StoreDigitalCodesPageView.d.ts +4 -1
  200. package/dist/features/stores/components/StoreDigitalCodesPageView.js +16 -12
  201. package/dist/features/stores/components/StoreLiveItemsListing.js +7 -0
  202. package/dist/features/stores/components/StoreLiveItemsPageView.d.ts +4 -1
  203. package/dist/features/stores/components/StoreLiveItemsPageView.js +16 -12
  204. package/dist/features/stores/components/StorePreOrdersListing.js +6 -11
  205. package/dist/features/stores/components/StorePreOrdersPageView.d.ts +4 -1
  206. package/dist/features/stores/components/StorePreOrdersPageView.js +17 -17
  207. package/dist/features/stores/components/StorePrizeDrawsPageView.d.ts +4 -8
  208. package/dist/features/stores/components/StorePrizeDrawsPageView.js +17 -22
  209. package/dist/features/stores/components/StoreProductsListing.d.ts +7 -1
  210. package/dist/features/stores/components/StoreProductsListing.js +3 -2
  211. package/dist/features/stores/components/StoreProductsPageView.d.ts +1 -1
  212. package/dist/features/stores/components/StoreProductsPageView.js +16 -21
  213. package/dist/features/stores/components/StoreReviewsListing.d.ts +4 -0
  214. package/dist/features/stores/components/StoreReviewsListing.js +7 -108
  215. package/dist/features/stores/schemas/index.d.ts +2 -2
  216. package/dist/features/tester/seed-data/coupons-tester-seed-data.js +3 -3
  217. package/dist/features/tester/seed-data/index.d.ts +1 -0
  218. package/dist/features/tester/seed-data/index.js +1 -0
  219. package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +2 -0
  220. package/dist/features/tester/seed-data/offers-tester-seed-data.js +61 -0
  221. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +939 -1
  222. package/dist/index.d.ts +9 -3
  223. package/dist/index.js +11 -2
  224. package/dist/next/routing/route-map.d.ts +2 -0
  225. package/dist/next/routing/route-map.js +5 -0
  226. package/dist/react/hooks/useListingTypeFlags.d.ts +12 -19
  227. package/dist/react/hooks/useListingTypeFlags.js +4 -9
  228. package/dist/schemas/registry.d.ts +0 -12
  229. package/dist/seed/claimed-coupons-seed-data.js +3 -3
  230. package/dist/seed/coupons-seed-data.js +29 -10
  231. package/dist/seed/faq-seed-data.js +2 -2
  232. package/dist/seed/manifest.js +2 -2
  233. package/dist/seed/reviews-seed-data.js +52 -0
  234. package/dist/server-entry.d.ts +1 -0
  235. package/dist/server-entry.js +1 -0
  236. package/dist/styles.css +55 -39
  237. package/dist/tailwind-utilities.css +1 -1
  238. package/dist/ui/components/Checkbox.d.ts +10 -1
  239. package/dist/ui/components/Checkbox.js +14 -2
  240. package/dist/ui/components/Checkbox.style.css +11 -0
  241. package/dist/ui/components/FilterChipGroup.d.ts +21 -3
  242. package/dist/ui/components/FilterChipGroup.js +33 -4
  243. package/dist/ui/components/HorizontalScroller.d.ts +1 -2
  244. package/dist/ui/components/HorizontalScroller.js +9 -5
  245. package/dist/ui/components/HorizontalScroller.style.css +43 -38
  246. package/package.json +1 -1
@@ -47,6 +47,8 @@ export declare const TABLE_KEYS: {
47
47
  readonly DELIVERY_METHOD: "deliveryMethod";
48
48
  readonly SPECIES: "species";
49
49
  readonly JURISDICTION: "jurisdiction";
50
+ readonly LIVE_SEX: "liveSex";
51
+ readonly LIVE_TRANSPORT_METHOD: "liveTransportMethod";
50
52
  };
51
53
  export type TableKey = (typeof TABLE_KEYS)[keyof typeof TABLE_KEYS];
52
54
  export declare const VIEW_MODE: {
@@ -50,6 +50,8 @@ export const TABLE_KEYS = {
50
50
  // Live-item-specific
51
51
  SPECIES: "species",
52
52
  JURISDICTION: "jurisdiction",
53
+ LIVE_SEX: "liveSex",
54
+ LIVE_TRANSPORT_METHOD: "liveTransportMethod",
53
55
  };
54
56
  export const VIEW_MODE = {
55
57
  GRID: "grid",
@@ -1,10 +1,10 @@
1
1
  "use client";
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { sortBy, sieveFilter, SIEVE_OP } from "../../../utils/sieve-builder";
4
4
  import { ACCOUNT_ENDPOINTS } from "../../../constants/api-endpoints";
5
5
  import { ROUTES } from "../../../constants/index";
6
6
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
7
- import { Button, Div, Grid, Stack, Text, TextLink } from "../../../ui";
7
+ import { Button, Div, FilterChipGroup, Grid, Stack, Text, TextLink } from "../../../ui";
8
8
  import { PaginatedSelect } from "../../../ui/components/PaginatedSelect";
9
9
  import { DataListingView } from "../../admin/components/DataListingView";
10
10
  import { OrderCard } from "../../orders/components/OrdersList";
@@ -16,6 +16,23 @@ const SORT_OPTIONS = [
16
16
  { value: sortBy("totalPrice", "DESC"), label: "Highest total" },
17
17
  { value: sortBy("totalPrice", "ASC"), label: "Lowest total" },
18
18
  ];
19
+ /**
20
+ * Lane tabs — the same auction > offer > standard split the cart and checkout
21
+ * use, viewed after the fact. Every `id` is EXACTLY a stored
22
+ * `OrderDocument.orderType` value (or "" for All): a tab id that doesn't
23
+ * match a real stored value returns zero rows forever with no error anywhere
24
+ * (CLAUDE.md Root Cause #33).
25
+ *
26
+ * "standard" is filtered in memory server-side, because orders written before
27
+ * `orderType` existed have no such field and a Firestore `==` would exclude
28
+ * every one of them.
29
+ */
30
+ const ORDER_LANE_TABS = [
31
+ { id: "", label: "All" },
32
+ { id: "standard", label: "Normal" },
33
+ { id: "auction", label: "Auction wins" },
34
+ { id: "offer", label: "Offer wins" },
35
+ ];
19
36
  const STATUS_OPTIONS = [
20
37
  { value: "pending", label: "Pending" },
21
38
  { value: "processing", label: "Processing" },
@@ -31,7 +48,7 @@ export function UserOrdersView({ onOrderClick }) {
31
48
  title: "My Orders",
32
49
  searchPlaceholder: "Search by order id…",
33
50
  emptyLabel: "You haven't placed any orders yet.",
34
- filterKeys: ["status"],
51
+ filterKeys: ["status", "orderType"],
35
52
  defaultSort: sortBy("createdAt", "DESC"),
36
53
  queryKey: ["user", "orders", "listing"],
37
54
  endpoint: ACCOUNT_ENDPOINTS.ORDERS,
@@ -39,8 +56,13 @@ export function UserOrdersView({ onOrderClick }) {
39
56
  hideTableView: true,
40
57
  mapRows: (response) => response.items ?? [],
41
58
  getTotal: (response, rows) => response.total ?? rows.length,
42
- buildFilters: (state) => (state.status ? sieveFilter("status", SIEVE_OP.EQ, state.status) : undefined),
43
- renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsx(Div, { children: _jsx(PaginatedSelect, { value: pendingFilters.status || null, onChange: (v) => setPendingFilters((p) => ({ ...p, status: v ?? "" })), options: STATUS_OPTIONS, placeholder: "All statuses", ariaLabel: "Filter by order status" }) })),
59
+ buildFilters: (state) => [
60
+ state.status ? sieveFilter("status", SIEVE_OP.EQ, state.status) : null,
61
+ state.orderType ? sieveFilter("orderType", SIEVE_OP.EQ, state.orderType) : null,
62
+ ]
63
+ .filter(Boolean)
64
+ .join(",") || undefined,
65
+ renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(Stack, { gap: "md", children: [_jsx(FilterChipGroup, { label: "Type", tabs: ORDER_LANE_TABS, value: pendingFilters.orderType ?? "", onChange: (id) => setPendingFilters((prev) => ({ ...prev, orderType: id })) }), _jsx(Div, { children: _jsx(PaginatedSelect, { value: pendingFilters.status || null, onChange: (v) => setPendingFilters((p) => ({ ...p, status: v ?? "" })), options: STATUS_OPTIONS, placeholder: "All statuses", ariaLabel: "Filter by order status" }) })] })),
44
66
  renderCards: (rows, _view, _selection, isLoading) => {
45
67
  if (isLoading) {
46
68
  return (_jsx(Stack, { gap: "md", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Div, { className: "h-24 animate-pulse border border-[var(--appkit-color-border)]", rounded: "xl" }, i))) }));
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { sortBy } from "@mohasinac/appkit/client";
4
- import { useState } from "react";
4
+ import { useUrlTable } from "../../../react/hooks/useUrlTable";
5
5
  import { useQueryClient } from "@tanstack/react-query";
6
6
  import { Badge, Div, FilterChipGroup, Span, Stack, Text, useToast } from "../../../ui";
7
7
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
@@ -47,20 +47,26 @@ const ADDRESS_COLUMNS = [
47
47
  render: (row) => _jsx(Span, { size: "sm", color: "muted", children: row.updatedAt }),
48
48
  },
49
49
  ];
50
- function buildAddressConfig(banStatus, onAction) {
50
+ function buildAddressConfig(onAction) {
51
51
  return {
52
52
  portal: "admin",
53
53
  title: "Address Management",
54
54
  searchPlaceholder: "Search by city or owner",
55
55
  emptyLabel: "No addresses found",
56
- filterKeys: [],
56
+ // URL-synced via filterKeys (2026-08-21). The ban-status chips used to
57
+ // live outside this config in local useState, so the selection was not in
58
+ // the URL, not shareable, not counted by activeFilterCount, and lost on
59
+ // back-navigation. banStatus is a plain query param rather than a Sieve
60
+ // clause, which is exactly what buildExtraParams is for.
61
+ filterKeys: ["banStatus"],
57
62
  defaultSort: sortBy("bannedAt", "DESC"),
58
- queryKey: ["admin", "addresses", banStatus],
59
- endpoint: banStatus
60
- ? `${ADMIN_ENDPOINTS.ADDRESSES}?banStatus=${encodeURIComponent(banStatus)}`
61
- : ADMIN_ENDPOINTS.ADDRESSES,
63
+ queryKey: ["admin", "addresses"],
64
+ endpoint: ADMIN_ENDPOINTS.ADDRESSES,
65
+ buildExtraParams: (state) => state.banStatus ? { banStatus: state.banStatus } : undefined,
62
66
  sortOptions: [
63
- { value: "bannedAt", label: "Flagged Date" },
67
+ // -bannedAt (newest flagged first) to match defaultSort — the option was
68
+ // "bannedAt" (oldest first), so the dropdown opened with nothing selected.
69
+ { value: sortBy("bannedAt", "DESC"), label: "Flagged Date" },
64
70
  { value: "city", label: "City" },
65
71
  ],
66
72
  columns: ADDRESS_COLUMNS,
@@ -119,7 +125,11 @@ function buildAddressConfig(banStatus, onAction) {
119
125
  };
120
126
  }
121
127
  export function AdminAddressesView(_props) {
122
- const [banStatus, setBanStatus] = useState("");
128
+ // Independent useUrlTable() against the same URL param DataListingView's own
129
+ // table reads via filterKeys — safe because useUrlTable holds no local state
130
+ // (the documented Root Cause #35 pattern).
131
+ const chipTable = useUrlTable({ defaults: {} });
132
+ const banStatus = chipTable.get("banStatus");
123
133
  const queryClient = useQueryClient();
124
134
  const { showToast } = useToast();
125
135
  const actionMutation = useApiMutation({
@@ -137,6 +147,6 @@ export function AdminAddressesView(_props) {
137
147
  const handleAction = (id, action) => {
138
148
  actionMutation.mutate({ id, action });
139
149
  };
140
- const config = buildAddressConfig(banStatus, handleAction);
141
- return (_jsxs(Stack, { gap: "md", children: [_jsx(Div, { padding: "inline", border: "default", className: "border-b", children: _jsx(FilterChipGroup, { label: "Status", tabs: BAN_STATUS_TABS, value: banStatus, onChange: (v) => setBanStatus(v) }) }), _jsx(DataListingView, { config: config })] }));
150
+ const config = buildAddressConfig(handleAction);
151
+ return (_jsxs(Stack, { gap: "md", children: [_jsx(Div, { padding: "inline", border: "default", className: "border-b", children: _jsx(FilterChipGroup, { label: "Status", tabs: BAN_STATUS_TABS, value: banStatus, onChange: (v) => chipTable.set("banStatus", v) }) }), _jsx(DataListingView, { config: config })] }));
142
152
  }
@@ -1,3 +1,12 @@
1
+ /**
2
+ * AdminArtView — admin browse of art listings.
3
+ *
4
+ * A thin wrapper over the shared per-listing-type config. This file (and its
5
+ * four siblings) used to carry a full hand-written ListingViewConfig; all five
6
+ * were the same config with a different listingType, and all five shipped
7
+ * an empty filterKeys array — no filter drawer at all — plus their own copy
8
+ * of a three-option sort array. See listing-type-listing-config.ts.
9
+ */
1
10
  import React from "react";
2
11
  import type { ListingLayoutProps } from "../../../ui";
3
12
  export type AdminArtViewProps = ListingLayoutProps;
@@ -1,50 +1,26 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  /**
4
- * AdminArtView — admin browse of art print listings (EMI/art-stickers session).
5
- * Mirrors AdminClassifiedView's thin config-driven pattern.
4
+ * AdminArtView — admin browse of art listings.
5
+ *
6
+ * A thin wrapper over the shared per-listing-type config. This file (and its
7
+ * four siblings) used to carry a full hand-written ListingViewConfig; all five
8
+ * were the same config with a different listingType, and all five shipped
9
+ * an empty filterKeys array — no filter drawer at all — plus their own copy
10
+ * of a three-option sort array. See listing-type-listing-config.ts.
6
11
  */
7
- import { sortBy } from "@mohasinac/appkit/client";
8
12
  import React from "react";
9
13
  import { ListingLayout } from "../../../ui";
10
- import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
11
- import { toRecordArray, toRelativeDate, toCurrency, toStringValue, } from "../hooks/useAdminListingData";
14
+ import { buildListingTypeListingConfig } from "../../products/config/listing-type-listing-config";
12
15
  import { DataListingView } from "./DataListingView";
13
- import { ROUTES } from "../../../next/routing/route-map";
14
- const ADMIN_ART_CONFIG = {
15
- portal: "admin",
16
+ const CONFIG = buildListingTypeListingConfig("art", {
16
17
  title: "Art",
17
18
  searchPlaceholder: "Search art prints by name or seller",
18
19
  emptyLabel: "No art listings",
19
- filterKeys: [],
20
- defaultSort: sortBy("createdAt", "DESC"),
21
- queryKey: ["admin", "art", "listing"],
22
- endpoint: ADMIN_ENDPOINTS.PRODUCTS,
23
- sortOptions: [
24
- { value: sortBy("createdAt", "DESC"), label: "Newest" },
25
- { value: sortBy("createdAt", "ASC"), label: "Oldest" },
26
- { value: "title", label: "Title A–Z" },
27
- ],
28
- mapRows: (response) => toRecordArray(response.items).map((item, index) => ({
29
- id: toStringValue(item.id, `art-${index}`),
30
- primary: toStringValue(item.title ?? item.productTitle, "Untitled print"),
31
- secondary: [
32
- toStringValue(item.sellerName, "Unknown seller"),
33
- toCurrency(item.price),
34
- ].join(" · "),
35
- status: toStringValue(item.status, "draft"),
36
- updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
37
- image: toStringValue(item.mainImage, "") || undefined,
38
- })),
39
- getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
40
- buildFilters: () => "listingType==art",
41
- // Art listings are products (filtered by listingType) — reuse the real
42
- // admin product edit page rather than leaving rows non-navigable.
43
- rowHrefTemplate: String(ROUTES.ADMIN.PRODUCTS_EDIT("{id}")),
44
- };
20
+ });
45
21
  export function AdminArtView({ children, ...props }) {
46
22
  if (React.Children.count(children) > 0) {
47
23
  return (_jsx(ListingLayout, { portal: "admin", ...props, children: children }));
48
24
  }
49
- return _jsx(DataListingView, { config: ADMIN_ART_CONFIG });
25
+ return _jsx(DataListingView, { config: CONFIG });
50
26
  }
@@ -1,3 +1,12 @@
1
+ /**
2
+ * AdminClassifiedView — admin browse of classified listings.
3
+ *
4
+ * A thin wrapper over the shared per-listing-type config. This file (and its
5
+ * four siblings) used to carry a full hand-written ListingViewConfig; all five
6
+ * were the same config with a different listingType, and all five shipped
7
+ * an empty filterKeys array — no filter drawer at all — plus their own copy
8
+ * of a three-option sort array. See listing-type-listing-config.ts.
9
+ */
1
10
  import React from "react";
2
11
  import type { ListingLayoutProps } from "../../../ui";
3
12
  export type AdminClassifiedViewProps = ListingLayoutProps;
@@ -1,50 +1,26 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  /**
4
- * AdminClassifiedView — admin browse of classified listings (W1-29).
5
- * Read-only summary with edit-handoff to the seller-portal edit route.
4
+ * AdminClassifiedView — admin browse of classified listings.
5
+ *
6
+ * A thin wrapper over the shared per-listing-type config. This file (and its
7
+ * four siblings) used to carry a full hand-written ListingViewConfig; all five
8
+ * were the same config with a different listingType, and all five shipped
9
+ * an empty filterKeys array — no filter drawer at all — plus their own copy
10
+ * of a three-option sort array. See listing-type-listing-config.ts.
6
11
  */
7
- import { sortBy } from "@mohasinac/appkit/client";
8
12
  import React from "react";
9
13
  import { ListingLayout } from "../../../ui";
10
- import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
11
- import { toRecordArray, toRelativeDate, toCurrency, toStringValue, } from "../hooks/useAdminListingData";
14
+ import { buildListingTypeListingConfig } from "../../products/config/listing-type-listing-config";
12
15
  import { DataListingView } from "./DataListingView";
13
- import { ROUTES } from "../../../next/routing/route-map";
14
- const ADMIN_CLASSIFIED_CONFIG = {
15
- portal: "admin",
16
+ const CONFIG = buildListingTypeListingConfig("classified", {
16
17
  title: "Classified",
17
18
  searchPlaceholder: "Search classified by name or seller",
18
19
  emptyLabel: "No classified listings",
19
- filterKeys: [],
20
- defaultSort: sortBy("createdAt", "DESC"),
21
- queryKey: ["admin", "classified", "listing"],
22
- endpoint: ADMIN_ENDPOINTS.PRODUCTS,
23
- sortOptions: [
24
- { value: sortBy("createdAt", "DESC"), label: "Newest" },
25
- { value: sortBy("createdAt", "ASC"), label: "Oldest" },
26
- { value: "title", label: "Title A–Z" },
27
- ],
28
- mapRows: (response) => toRecordArray(response.items).map((item, index) => ({
29
- id: toStringValue(item.id, `classified-${index}`),
30
- primary: toStringValue(item.title ?? item.productTitle, "Untitled classified"),
31
- secondary: [
32
- toStringValue(item.sellerName, "Unknown seller"),
33
- toCurrency(item.price),
34
- ].join(" · "),
35
- status: toStringValue(item.status, "draft"),
36
- updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
37
- image: toStringValue(item.mainImage, "") || undefined,
38
- })),
39
- getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
40
- buildFilters: () => "listingType==classified",
41
- // Classified listings are products (filtered by listingType) — reuse the
42
- // real admin product edit page rather than leaving rows non-navigable.
43
- rowHrefTemplate: String(ROUTES.ADMIN.PRODUCTS_EDIT("{id}")),
44
- };
20
+ });
45
21
  export function AdminClassifiedView({ children, ...props }) {
46
22
  if (React.Children.count(children) > 0) {
47
23
  return (_jsx(ListingLayout, { portal: "admin", ...props, children: children }));
48
24
  }
49
- return _jsx(DataListingView, { config: ADMIN_CLASSIFIED_CONFIG });
25
+ return _jsx(DataListingView, { config: CONFIG });
50
26
  }
@@ -81,7 +81,6 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
81
81
  const [isActive, setIsActive] = React.useState(true);
82
82
  // restrictions
83
83
  const [firstTimeOnly, setFirstTimeOnly] = React.useState(false);
84
- const [combinable, setCombinable] = React.useState(false);
85
84
  const [appliesToAuctions, setAppliesToAuctions] = React.useState(false);
86
85
  const [applicableProducts, setApplicableProducts] = React.useState([]);
87
86
  const [applicableCategories, setApplicableCategories] = React.useState([]);
@@ -122,7 +121,6 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
122
121
  setEndDate(toDateInputValue(c.validity?.endDate));
123
122
  setIsActive(c.validity?.isActive ?? false);
124
123
  setFirstTimeOnly(c.restrictions?.firstTimeUserOnly ?? false);
125
- setCombinable(c.restrictions?.combineWithSellerCoupons ?? false);
126
124
  setApplicableProducts(Array.isArray(c.restrictions?.applicableProducts) ? c.restrictions.applicableProducts : []);
127
125
  setApplicableCategories(Array.isArray(c.restrictions?.applicableCategories) ? c.restrictions.applicableCategories : []);
128
126
  setAppliesToAuctions(c.applicableToAuctions ?? false);
@@ -162,7 +160,6 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
162
160
  },
163
161
  restrictions: {
164
162
  firstTimeUserOnly: firstTimeOnly,
165
- combineWithSellerCoupons: combinable,
166
163
  ...(applicableProducts.length > 0 && { applicableProducts }),
167
164
  ...(applicableCategories.length > 0 && { applicableCategories }),
168
165
  },
@@ -208,7 +205,7 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
208
205
  const formSection = (_jsxs(_Fragment, { children: [_jsx(Form, { schema: couponFormSchema, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(Select, { label: "Coupon type", options: TYPE_OPTIONS, value: type, onValueChange: (v) => setType(v), required: true }), _jsx(FieldInput, { name: "name", label: "Campaign name", value: name, onChange: (v) => handleNameChange(v), required: true, placeholder: "e.g. Summer Sale 20%" }), !isEdit && (_jsx(Input, { label: "Coupon code", value: code, onChange: (e) => {
209
206
  setCode(toCouponCode(e.target.value));
210
207
  setCodeManual(true);
211
- }, required: true, placeholder: "e.g. SUMMER20", helperText: "Auto-generated from name. Uppercase alphanumeric + hyphens only." })), isEdit && (_jsx(Input, { label: "Coupon code", value: code, disabled: true, helperText: "Code cannot be changed after creation." })), _jsx(Input, { label: "Description (optional)", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Internal notes about this coupon" }), _jsx(CouponDiscountFields, { type: type, discountValue: discountValue, setDiscountValue: setDiscountValue, maxDiscount: maxDiscount, setMaxDiscount: setMaxDiscount, minPurchase: minPurchase, setMinPurchase: setMinPurchase, buyQty: buyQty, setBuyQty: setBuyQty, getQty: getQty, setGetQty: setGetQty, discountLabel: discountLabel }), _jsx(CouponValidityFields, { startDate: startDate, setStartDate: setStartDate, endDate: endDate, setEndDate: setEndDate, isActive: isActive, setIsActive: setIsActive, totalLimit: totalLimit, setTotalLimit: setTotalLimit, perUserLimit: perUserLimit, setPerUserLimit: setPerUserLimit, isEdit: isEdit, currentUsage: currentUsage }), _jsx(Toggle, { label: "First-time users only", checked: firstTimeOnly, onChange: setFirstTimeOnly }), _jsx(Toggle, { label: "Allow stacking with seller coupons", checked: combinable, onChange: setCombinable }), _jsx(Toggle, { label: "Applies to auctions", checked: appliesToAuctions, onChange: setAppliesToAuctions }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Applicable products (optional)" }), _jsx(ProductInlineSelect, { scope: "admin", multiple: true, value: applicableProducts, onChange: setApplicableProducts, placeholder: "Restrict to specific products\u2026" }), _jsx(Text, { size: "xs", color: "muted", children: "Leave empty to apply the coupon to every eligible product." })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Applicable categories (optional)" }), _jsx(CategoryInlineSelect, { value: "", onChange: (id) => {
208
+ }, required: true, placeholder: "e.g. SUMMER20", helperText: "Auto-generated from name. Uppercase alphanumeric + hyphens only." })), isEdit && (_jsx(Input, { label: "Coupon code", value: code, disabled: true, helperText: "Code cannot be changed after creation." })), _jsx(Input, { label: "Description (optional)", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Internal notes about this coupon" }), _jsx(CouponDiscountFields, { type: type, discountValue: discountValue, setDiscountValue: setDiscountValue, maxDiscount: maxDiscount, setMaxDiscount: setMaxDiscount, minPurchase: minPurchase, setMinPurchase: setMinPurchase, buyQty: buyQty, setBuyQty: setBuyQty, getQty: getQty, setGetQty: setGetQty, discountLabel: discountLabel }), _jsx(CouponValidityFields, { startDate: startDate, setStartDate: setStartDate, endDate: endDate, setEndDate: setEndDate, isActive: isActive, setIsActive: setIsActive, totalLimit: totalLimit, setTotalLimit: setTotalLimit, perUserLimit: perUserLimit, setPerUserLimit: setPerUserLimit, isEdit: isEdit, currentUsage: currentUsage }), _jsx(Toggle, { label: "First-time users only", checked: firstTimeOnly, onChange: setFirstTimeOnly }), _jsx(Toggle, { label: "Applies to auctions", checked: appliesToAuctions, onChange: setAppliesToAuctions }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Applicable products (optional)" }), _jsx(ProductInlineSelect, { scope: "admin", multiple: true, value: applicableProducts, onChange: setApplicableProducts, placeholder: "Restrict to specific products\u2026" }), _jsx(Text, { size: "xs", color: "muted", children: "Leave empty to apply the coupon to every eligible product." })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Applicable categories (optional)" }), _jsx(CategoryInlineSelect, { value: "", onChange: (id) => {
212
209
  if (!id || applicableCategories.includes(id))
213
210
  return;
214
211
  setApplicableCategories([...applicableCategories, id]);
@@ -1,3 +1,12 @@
1
+ /**
2
+ * AdminDigitalCodesView — admin browse of digital codes listings.
3
+ *
4
+ * A thin wrapper over the shared per-listing-type config. This file (and its
5
+ * four siblings) used to carry a full hand-written ListingViewConfig; all five
6
+ * were the same config with a different listingType, and all five shipped
7
+ * an empty filterKeys array — no filter drawer at all — plus their own copy
8
+ * of a three-option sort array. See listing-type-listing-config.ts.
9
+ */
1
10
  import React from "react";
2
11
  import type { ListingLayoutProps } from "../../../ui";
3
12
  export type AdminDigitalCodesViewProps = ListingLayoutProps;
@@ -1,57 +1,26 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  /**
4
- * AdminDigitalCodesView — admin browse of digital-code listings (W1-29).
4
+ * AdminDigitalCodesView — admin browse of digital codes listings.
5
+ *
6
+ * A thin wrapper over the shared per-listing-type config. This file (and its
7
+ * four siblings) used to carry a full hand-written ListingViewConfig; all five
8
+ * were the same config with a different listingType, and all five shipped
9
+ * an empty filterKeys array — no filter drawer at all — plus their own copy
10
+ * of a three-option sort array. See listing-type-listing-config.ts.
5
11
  */
6
- import { sortBy } from "@mohasinac/appkit/client";
7
12
  import React from "react";
8
13
  import { ListingLayout } from "../../../ui";
9
- import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
10
- import { toRecordArray, toRelativeDate, toCurrency, toStringValue, } from "../hooks/useAdminListingData";
14
+ import { buildListingTypeListingConfig } from "../../products/config/listing-type-listing-config";
11
15
  import { DataListingView } from "./DataListingView";
12
- import { ROUTES } from "../../../next/routing/route-map";
13
- const ADMIN_DIGITAL_CODES_CONFIG = {
14
- portal: "admin",
16
+ const CONFIG = buildListingTypeListingConfig("digital-code", {
15
17
  title: "Digital Codes",
16
18
  searchPlaceholder: "Search digital code listings",
17
19
  emptyLabel: "No digital code listings",
18
- filterKeys: [],
19
- defaultSort: sortBy("createdAt", "DESC"),
20
- queryKey: ["admin", "digital-codes", "listing"],
21
- endpoint: ADMIN_ENDPOINTS.PRODUCTS,
22
- sortOptions: [
23
- { value: sortBy("createdAt", "DESC"), label: "Newest" },
24
- { value: sortBy("createdAt", "ASC"), label: "Oldest" },
25
- { value: "title", label: "Title A–Z" },
26
- ],
27
- mapRows: (response) => toRecordArray(response.items).map((item, index) => {
28
- const dc = (item.digitalCode ?? {});
29
- return {
30
- id: toStringValue(item.id, `dc-${index}`),
31
- primary: toStringValue(item.title ?? item.productTitle, "Untitled digital code"),
32
- secondary: [
33
- toStringValue(item.sellerName, "Unknown seller"),
34
- toCurrency(item.price),
35
- typeof dc.codesAvailable === "number"
36
- ? `${dc.codesAvailable} avail`
37
- : null,
38
- ]
39
- .filter(Boolean)
40
- .join(" · "),
41
- status: toStringValue(item.status, "draft"),
42
- updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
43
- image: toStringValue(item.mainImage, "") || undefined,
44
- };
45
- }),
46
- getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
47
- buildFilters: () => "listingType==digital-code",
48
- // Digital-code listings are products (filtered by listingType) — reuse
49
- // the real admin product edit page rather than leaving rows non-navigable.
50
- rowHrefTemplate: String(ROUTES.ADMIN.PRODUCTS_EDIT("{id}")),
51
- };
20
+ });
52
21
  export function AdminDigitalCodesView({ children, ...props }) {
53
22
  if (React.Children.count(children) > 0) {
54
23
  return (_jsx(ListingLayout, { portal: "admin", ...props, children: children }));
55
24
  }
56
- return _jsx(DataListingView, { config: ADMIN_DIGITAL_CODES_CONFIG });
25
+ return _jsx(DataListingView, { config: CONFIG });
57
26
  }
@@ -1,3 +1,12 @@
1
+ /**
2
+ * AdminLiveView — admin browse of live items listings.
3
+ *
4
+ * A thin wrapper over the shared per-listing-type config. This file (and its
5
+ * four siblings) used to carry a full hand-written ListingViewConfig; all five
6
+ * were the same config with a different listingType, and all five shipped
7
+ * an empty filterKeys array — no filter drawer at all — plus their own copy
8
+ * of a three-option sort array. See listing-type-listing-config.ts.
9
+ */
1
10
  import React from "react";
2
11
  import type { ListingLayoutProps } from "../../../ui";
3
12
  export type AdminLiveViewProps = ListingLayoutProps;
@@ -1,56 +1,26 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  /**
4
- * AdminLiveView — admin browse of live-item listings (W1-29).
4
+ * AdminLiveView — admin browse of live items listings.
5
+ *
6
+ * A thin wrapper over the shared per-listing-type config. This file (and its
7
+ * four siblings) used to carry a full hand-written ListingViewConfig; all five
8
+ * were the same config with a different listingType, and all five shipped
9
+ * an empty filterKeys array — no filter drawer at all — plus their own copy
10
+ * of a three-option sort array. See listing-type-listing-config.ts.
5
11
  */
6
- import { sortBy } from "@mohasinac/appkit/client";
7
12
  import React from "react";
8
13
  import { ListingLayout } from "../../../ui";
9
- import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
10
- import { toRecordArray, toRelativeDate, toCurrency, toStringValue, } from "../hooks/useAdminListingData";
14
+ import { buildListingTypeListingConfig } from "../../products/config/listing-type-listing-config";
11
15
  import { DataListingView } from "./DataListingView";
12
- import { ROUTES } from "../../../next/routing/route-map";
13
- const ADMIN_LIVE_CONFIG = {
14
- portal: "admin",
16
+ const CONFIG = buildListingTypeListingConfig("live", {
15
17
  title: "Live Items",
16
18
  searchPlaceholder: "Search live items",
17
19
  emptyLabel: "No live item listings",
18
- filterKeys: [],
19
- defaultSort: sortBy("createdAt", "DESC"),
20
- queryKey: ["admin", "live-items", "listing"],
21
- endpoint: ADMIN_ENDPOINTS.PRODUCTS,
22
- sortOptions: [
23
- { value: sortBy("createdAt", "DESC"), label: "Newest" },
24
- { value: sortBy("createdAt", "ASC"), label: "Oldest" },
25
- { value: "title", label: "Title A–Z" },
26
- ],
27
- mapRows: (response) => toRecordArray(response.items).map((item, index) => {
28
- const live = (item.liveItem ?? {});
29
- return {
30
- id: toStringValue(item.id, `live-${index}`),
31
- primary: toStringValue(item.title ?? item.productTitle, "Untitled live item"),
32
- secondary: [
33
- toStringValue(item.sellerName, "Unknown seller"),
34
- toCurrency(item.price),
35
- toStringValue(live.species, ""),
36
- Boolean(live.vendorVerified) ? "verified" : "pending",
37
- ]
38
- .filter(Boolean)
39
- .join(" · "),
40
- status: toStringValue(item.status, "draft"),
41
- updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
42
- image: toStringValue(item.mainImage, "") || undefined,
43
- };
44
- }),
45
- getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
46
- buildFilters: () => "listingType==live",
47
- // Live-item listings are products (filtered by listingType) — reuse the
48
- // real admin product edit page rather than leaving rows non-navigable.
49
- rowHrefTemplate: String(ROUTES.ADMIN.PRODUCTS_EDIT("{id}")),
50
- };
20
+ });
51
21
  export function AdminLiveView({ children, ...props }) {
52
22
  if (React.Children.count(children) > 0) {
53
23
  return (_jsx(ListingLayout, { portal: "admin", ...props, children: children }));
54
24
  }
55
- return _jsx(DataListingView, { config: ADMIN_LIVE_CONFIG });
25
+ return _jsx(DataListingView, { config: CONFIG });
56
26
  }
@@ -1,3 +1,4 @@
1
+ import { type OrderAddonBadgesOrder } from "../../orders/components/OrderAddonBadges";
1
2
  import React from "react";
2
3
  export interface AdminOrderItemRow {
3
4
  productId: string;
@@ -23,5 +24,11 @@ export interface AdminOrderEditorViewProps {
23
24
  paymentUpiMismatch?: boolean;
24
25
  buyerMarkedPaid?: boolean;
25
26
  buyerFraudAgreementAccepted?: boolean;
27
+ /**
28
+ * Paid add-ons + applied coupon for this order. Operational, not decorative:
29
+ * `whatsappNotifyAddon` is who a status change should message, and the
30
+ * coupon is what an admin needs when reconciling a discounted order.
31
+ */
32
+ addons?: OrderAddonBadgesOrder;
26
33
  }
27
- export declare function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, items, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, displayedUpiId, buyerReportedUpiId, paymentUpiMismatch, buyerMarkedPaid, buyerFraudAgreementAccepted, }: AdminOrderEditorViewProps): React.JSX.Element;
34
+ export declare function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, items, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, displayedUpiId, buyerReportedUpiId, paymentUpiMismatch, buyerMarkedPaid, buyerFraudAgreementAccepted, addons, }: AdminOrderEditorViewProps): React.JSX.Element;
@@ -1,5 +1,6 @@
1
1
  "use client";
2
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { OrderAddonBadges } from "../../orders/components/OrderAddonBadges";
3
4
  import { useApiMutation } from "@mohasinac/appkit/client";
4
5
  import React, { useState } from "react";
5
6
  import { normalizeError } from "../../../errors/normalize";
@@ -29,7 +30,7 @@ const CARRIER_OPTIONS = [
29
30
  { label: "Other", value: "Other" },
30
31
  ];
31
32
  // --- Component ---------------------------------------------------------------
32
- export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, items, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, displayedUpiId, buyerReportedUpiId, paymentUpiMismatch, buyerMarkedPaid, buyerFraudAgreementAccepted, }) {
33
+ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, items, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, displayedUpiId, buyerReportedUpiId, paymentUpiMismatch, buyerMarkedPaid, buyerFraudAgreementAccepted, addons, }) {
33
34
  const queryClient = useQueryClient();
34
35
  const { showToast } = useToast();
35
36
  const [status, setStatus] = React.useState(currentStatus ?? "pending");
@@ -136,5 +137,5 @@ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, curre
136
137
  return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: orderLabel ? `Order: ${orderLabel}` : "Update Order", children: _jsxs(Form, { onSubmit: (e) => {
137
138
  e.preventDefault();
138
139
  saveMutation.mutate();
139
- }, spacing: "md", padding: "md", children: [items && items.length > 0 && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Label, { size: "sm", weight: "medium", color: "primary", children: ["Items (", items.length, ")"] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: items.map((item, i) => (_jsxs(Div, { layout: "flex", align: "center", gap: "3", padding: "sm", children: [_jsx(Div, { className: "h-10 w-10 shrink-0", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title, size: "thumbnail" }) }), _jsxs(Div, { className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.title }), _jsxs(Text, { size: "xs", color: "muted", children: ["Qty: ", item.quantity, " \u00D7 ", toCurrency(item.unitPrice)] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "semibold", children: toCurrency(item.totalPrice) })] }, item.productId || i))) })] })), _jsx(Select, { label: "Order status", options: STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsx(Input, { label: "Tracking number (optional)", value: trackingNumber, onChange: (e) => setTrackingNumber(e.target.value), placeholder: "e.g. DEL1234567890IN" }), _jsx(Select, { label: "Carrier (optional)", options: CARRIER_OPTIONS, value: carrier, onValueChange: setCarrier }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Internal note (optional)" }), _jsx(Textarea, { value: notes, onChange: (e) => setNotes(e.target.value), rows: 3, placeholder: "Reason for status change, escalation notes\u2026" })] }), (status === "refunded" || status === "return_requested") && (_jsx(Input, { label: "Refund amount \u20B9 (optional)", type: "number", min: "0", step: "0.01", value: refundAmount, onChange: (e) => setRefundAmount(e.target.value), placeholder: "e.g. 499.00" })), isCashOrUpi && (_jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Payment Proof" }), paymentProofUrl ? (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { border: "default", rounded: "lg", overflow: "hidden", children: _jsx(MediaImage, { src: paymentProofUrl, alt: "Payment screenshot", size: "card" }) }), paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", children: ["UTR: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: paymentTransactionId })] })), needsVerification && (_jsxs(Stack, { gap: "xs", children: [(displayedUpiId || buyerReportedUpiId) && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "xs", color: "muted", children: ["Expected UPI: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: displayedUpiId || "—" }), " · ", "Buyer reported: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerReportedUpiId || "—" })] }), paymentUpiMismatch && (_jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-error/20", surface: "danger-surface", children: _jsx(Text, { size: "xs", className: "text-error", weight: "semibold", children: "UPI mismatch \u2014 buyer-reported ID doesn't match the one shown for this order." }) }))] })), _jsxs(Text, { size: "xs", color: "muted", children: ["Buyer marked as paid: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerMarkedPaid ? "Yes" : "No" }), " · ", "Fraud agreement accepted: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerFraudAgreementAccepted ? "Yes" : "No" })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Review note (required for re-upload / reject)" }), _jsx(Textarea, { value: reviewNote, onChange: (e) => setReviewNote(e.target.value), rows: 2, placeholder: "e.g. Screenshot is blurry \u2014 amount not readable / UPI ID doesn't match order" })] }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["verify-payment"], onClick: handleVerifyPayment, isLoading: isVerifyingPayment, disabled: isVerifyingPayment, variant: "primary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["request-payment-reupload"], onClick: handleRequestReupload, isLoading: isRequestingReupload, disabled: isRequestingReupload || !reviewNote.trim(), variant: "secondary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["reject-payment-fraud"], onClick: handleRejectFraud, isLoading: isRejectingFraud, disabled: isRejectingFraud || !reviewNote.trim(), variant: "danger", className: "w-full" })] })), !needsVerification && paymentStatus === "paid" && (_jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-success/20", surface: "success-surface", children: _jsx(Text, { size: "xs", className: "text-success", weight: "medium", children: "Payment verified" }) }))] })) : (_jsx(Text, { size: "xs", color: "faint", children: "No proof uploaded yet." }))] })), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", isLoading: saveMutation.isPending, disabled: !orderId || saveMutation.isPending, children: "Save changes" })] })] }) }));
140
+ }, spacing: "md", padding: "md", children: [addons && _jsx(OrderAddonBadges, { order: addons, variant: "detail" }), items && items.length > 0 && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Label, { size: "sm", weight: "medium", color: "primary", children: ["Items (", items.length, ")"] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: items.map((item, i) => (_jsxs(Div, { layout: "flex", align: "center", gap: "3", padding: "sm", children: [_jsx(Div, { className: "h-10 w-10 shrink-0", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title, size: "thumbnail" }) }), _jsxs(Div, { className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.title }), _jsxs(Text, { size: "xs", color: "muted", children: ["Qty: ", item.quantity, " \u00D7 ", toCurrency(item.unitPrice)] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "semibold", children: toCurrency(item.totalPrice) })] }, item.productId || i))) })] })), _jsx(Select, { label: "Order status", options: STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsx(Input, { label: "Tracking number (optional)", value: trackingNumber, onChange: (e) => setTrackingNumber(e.target.value), placeholder: "e.g. DEL1234567890IN" }), _jsx(Select, { label: "Carrier (optional)", options: CARRIER_OPTIONS, value: carrier, onValueChange: setCarrier }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Internal note (optional)" }), _jsx(Textarea, { value: notes, onChange: (e) => setNotes(e.target.value), rows: 3, placeholder: "Reason for status change, escalation notes\u2026" })] }), (status === "refunded" || status === "return_requested") && (_jsx(Input, { label: "Refund amount \u20B9 (optional)", type: "number", min: "0", step: "0.01", value: refundAmount, onChange: (e) => setRefundAmount(e.target.value), placeholder: "e.g. 499.00" })), isCashOrUpi && (_jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Payment Proof" }), paymentProofUrl ? (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { border: "default", rounded: "lg", overflow: "hidden", children: _jsx(MediaImage, { src: paymentProofUrl, alt: "Payment screenshot", size: "card" }) }), paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", children: ["UTR: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: paymentTransactionId })] })), needsVerification && (_jsxs(Stack, { gap: "xs", children: [(displayedUpiId || buyerReportedUpiId) && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "xs", color: "muted", children: ["Expected UPI: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: displayedUpiId || "—" }), " · ", "Buyer reported: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerReportedUpiId || "—" })] }), paymentUpiMismatch && (_jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-error/20", surface: "danger-surface", children: _jsx(Text, { size: "xs", className: "text-error", weight: "semibold", children: "UPI mismatch \u2014 buyer-reported ID doesn't match the one shown for this order." }) }))] })), _jsxs(Text, { size: "xs", color: "muted", children: ["Buyer marked as paid: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerMarkedPaid ? "Yes" : "No" }), " · ", "Fraud agreement accepted: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerFraudAgreementAccepted ? "Yes" : "No" })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Review note (required for re-upload / reject)" }), _jsx(Textarea, { value: reviewNote, onChange: (e) => setReviewNote(e.target.value), rows: 2, placeholder: "e.g. Screenshot is blurry \u2014 amount not readable / UPI ID doesn't match order" })] }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["verify-payment"], onClick: handleVerifyPayment, isLoading: isVerifyingPayment, disabled: isVerifyingPayment, variant: "primary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["request-payment-reupload"], onClick: handleRequestReupload, isLoading: isRequestingReupload, disabled: isRequestingReupload || !reviewNote.trim(), variant: "secondary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["reject-payment-fraud"], onClick: handleRejectFraud, isLoading: isRejectingFraud, disabled: isRejectingFraud || !reviewNote.trim(), variant: "danger", className: "w-full" })] })), !needsVerification && paymentStatus === "paid" && (_jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-success/20", surface: "success-surface", children: _jsx(Text, { size: "xs", className: "text-success", weight: "medium", children: "Payment verified" }) }))] })) : (_jsx(Text, { size: "xs", color: "faint", children: "No proof uploaded yet." }))] })), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", isLoading: saveMutation.isPending, disabled: !orderId || saveMutation.isPending, children: "Save changes" })] })] }) }));
140
141
  }
@@ -122,6 +122,19 @@ export function AdminOrdersView({ children, ...props }) {
122
122
  paymentUpiMismatch: Boolean(item.paymentUpiMismatch),
123
123
  buyerMarkedPaid: Boolean(item.buyerMarkedPaid),
124
124
  buyerFraudAgreementAccepted: Boolean(item.buyerFraudAgreementAccepted),
125
+ // Straight off the order document — the admin list route returns
126
+ // raw docs, so these need no serializer entry (cf. Root Cause #38).
127
+ addons: {
128
+ whatsappNotifyAddon: Boolean(item.whatsappNotifyAddon),
129
+ whatsappNotifyFee: Number(item.whatsappNotifyFee ?? 0) || undefined,
130
+ giftWrapAddon: Boolean(item.giftWrapAddon),
131
+ giftWrapFee: Number(item.giftWrapFee ?? 0) || undefined,
132
+ giftWrapMessage: toStringValue(item.giftWrapMessage) || undefined,
133
+ shipmentProtectionAddon: Boolean(item.shipmentProtectionAddon),
134
+ shipmentProtectionFee: Number(item.shipmentProtectionFee ?? 0) || undefined,
135
+ couponCode: toStringValue(item.couponCode) || undefined,
136
+ couponDiscount: Number(item.couponDiscount ?? 0) || undefined,
137
+ },
125
138
  };
126
139
  }),
127
140
  getTotal: (response, mappedRows) => typeof response.meta?.total === "number" ? response.meta.total : mappedRows.length,
@@ -198,5 +211,5 @@ export function AdminOrdersView({ children, ...props }) {
198
211
  ] })),
199
212
  renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(_Fragment, { children: [_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_ORDER_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id, paymentReview: "" })) }), _jsx(FilterChipGroup, { label: "Manual payment", tabs: ADMIN_ORDER_PAYMENT_REVIEW_TABS, value: pendingFilters.paymentReview ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, paymentReview: id, status: "" })) })] })),
200
213
  };
201
- return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status, items: selectedRow?.items, paymentProofUrl: selectedRow?.paymentProofUrl, paymentTransactionId: selectedRow?.paymentTransactionId, paymentMethod: selectedRow?.paymentMethod, paymentStatus: selectedRow?.paymentStatus, displayedUpiId: selectedRow?.displayedUpiId, buyerReportedUpiId: selectedRow?.buyerReportedUpiId, paymentUpiMismatch: selectedRow?.paymentUpiMismatch, buyerMarkedPaid: selectedRow?.buyerMarkedPaid, buyerFraudAgreementAccepted: selectedRow?.buyerFraudAgreementAccepted })] }));
214
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status, items: selectedRow?.items, paymentProofUrl: selectedRow?.paymentProofUrl, paymentTransactionId: selectedRow?.paymentTransactionId, paymentMethod: selectedRow?.paymentMethod, paymentStatus: selectedRow?.paymentStatus, displayedUpiId: selectedRow?.displayedUpiId, buyerReportedUpiId: selectedRow?.buyerReportedUpiId, paymentUpiMismatch: selectedRow?.paymentUpiMismatch, buyerMarkedPaid: selectedRow?.buyerMarkedPaid, buyerFraudAgreementAccepted: selectedRow?.buyerFraudAgreementAccepted, addons: selectedRow?.addons })] }));
202
215
  }