@mohasinac/appkit 4.11.3 → 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 (103) hide show
  1. package/dist/_internal/server/features/checkout/actions.d.ts +36 -3
  2. package/dist/_internal/server/features/checkout/actions.js +122 -44
  3. package/dist/_internal/server/features/orders/adapters.js +13 -0
  4. package/dist/_internal/server/features/products/list-public.d.ts +28 -0
  5. package/dist/_internal/server/features/products/list-public.js +92 -0
  6. package/dist/_internal/server/jobs/core/auctionSettlement.js +5 -2
  7. package/dist/_internal/shared/checkout/lanes.d.ts +20 -0
  8. package/dist/_internal/shared/checkout/lanes.js +28 -0
  9. package/dist/_internal/shared/features/checkout/config.d.ts +4 -0
  10. package/dist/_internal/shared/features/checkout/config.js +8 -0
  11. package/dist/_internal/shared/fees/calculator.d.ts +34 -2
  12. package/dist/_internal/shared/fees/calculator.js +45 -1
  13. package/dist/client.d.ts +7 -1
  14. package/dist/client.js +4 -1
  15. package/dist/constants/api-endpoints.d.ts +6 -0
  16. package/dist/constants/api-endpoints.js +2 -0
  17. package/dist/features/admin/components/AdminAddressesView.js +18 -10
  18. package/dist/features/admin/components/AdminArtView.d.ts +9 -0
  19. package/dist/features/admin/components/AdminArtView.js +11 -35
  20. package/dist/features/admin/components/AdminClassifiedView.d.ts +9 -0
  21. package/dist/features/admin/components/AdminClassifiedView.js +11 -35
  22. package/dist/features/admin/components/AdminDigitalCodesView.d.ts +9 -0
  23. package/dist/features/admin/components/AdminDigitalCodesView.js +11 -42
  24. package/dist/features/admin/components/AdminLiveView.d.ts +9 -0
  25. package/dist/features/admin/components/AdminLiveView.js +11 -41
  26. package/dist/features/admin/components/AdminOrderEditorView.d.ts +8 -1
  27. package/dist/features/admin/components/AdminOrderEditorView.js +4 -3
  28. package/dist/features/admin/components/AdminOrdersView.js +14 -1
  29. package/dist/features/admin/components/AdminPaymentMethodsView.js +15 -10
  30. package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
  31. package/dist/features/admin/components/AdminSiteSettingsView.js +4 -2
  32. package/dist/features/admin/components/AdminStickersView.d.ts +9 -0
  33. package/dist/features/admin/components/AdminStickersView.js +11 -35
  34. package/dist/features/admin/hooks/useAdminListing.js +7 -2
  35. package/dist/features/admin/schemas/firestore.d.ts +5 -0
  36. package/dist/features/admin/schemas/firestore.js +1 -0
  37. package/dist/features/auctions/actions/bid-actions.js +2 -2
  38. package/dist/features/cart/components/CartDrawer.d.ts +9 -1
  39. package/dist/features/cart/components/CartDrawer.js +4 -3
  40. package/dist/features/cart/components/CartPriceBreakdown.d.ts +61 -0
  41. package/dist/features/cart/components/CartPriceBreakdown.js +25 -0
  42. package/dist/features/cart/components/StoreAddonsPicker.d.ts +47 -0
  43. package/dist/features/cart/components/StoreAddonsPicker.js +21 -0
  44. package/dist/features/cart/components/index.d.ts +4 -0
  45. package/dist/features/cart/components/index.js +2 -0
  46. package/dist/features/cart/repository/cart.repository.d.ts +20 -2
  47. package/dist/features/cart/repository/cart.repository.js +49 -0
  48. package/dist/features/cart/schemas/firestore.d.ts +26 -0
  49. package/dist/features/layout/BottomActions.js +37 -4
  50. package/dist/features/layout/BottomActionsContext.d.ts +12 -0
  51. package/dist/features/layout/BottomActionsContext.js +5 -0
  52. package/dist/features/layout/hooks/useBottomActions.d.ts +6 -0
  53. package/dist/features/layout/hooks/useBottomActions.js +11 -1
  54. package/dist/features/orders/components/OrderAddonBadges.d.ts +36 -0
  55. package/dist/features/orders/components/OrderAddonBadges.js +28 -0
  56. package/dist/features/orders/components/index.d.ts +1 -0
  57. package/dist/features/orders/components/index.js +1 -0
  58. package/dist/features/orders/repository/orders.repository.d.ts +32 -0
  59. package/dist/features/orders/repository/orders.repository.js +14 -0
  60. package/dist/features/orders/types/index.d.ts +22 -0
  61. package/dist/features/products/components/ArtStickersListView.js +5 -4
  62. package/dist/features/products/components/PrizeDrawsListingView.js +18 -33
  63. package/dist/features/products/components/ProductsIndexListing.js +87 -11
  64. package/dist/features/products/components/ProductsIndexPageView.js +14 -2
  65. package/dist/features/products/config/listing-type-listing-config.d.ts +31 -0
  66. package/dist/features/products/config/listing-type-listing-config.js +74 -0
  67. package/dist/features/products/hooks/useProducts.js +14 -0
  68. package/dist/features/products/types/index.d.ts +15 -0
  69. package/dist/features/seller/components/SellerOrdersView.js +2 -1
  70. package/dist/features/stores/components/StoreArtStickersPageView.d.ts +21 -0
  71. package/dist/features/stores/components/StoreArtStickersPageView.js +30 -0
  72. package/dist/features/stores/components/StoreAuctionsPageView.d.ts +4 -1
  73. package/dist/features/stores/components/StoreAuctionsPageView.js +17 -17
  74. package/dist/features/stores/components/StoreClassifiedsListing.js +7 -0
  75. package/dist/features/stores/components/StoreClassifiedsPageView.d.ts +4 -1
  76. package/dist/features/stores/components/StoreClassifiedsPageView.js +16 -12
  77. package/dist/features/stores/components/StoreDetailLayoutView.js +4 -3
  78. package/dist/features/stores/components/StoreDigitalCodesListing.js +5 -0
  79. package/dist/features/stores/components/StoreDigitalCodesPageView.d.ts +4 -1
  80. package/dist/features/stores/components/StoreDigitalCodesPageView.js +16 -12
  81. package/dist/features/stores/components/StoreLiveItemsListing.js +7 -0
  82. package/dist/features/stores/components/StoreLiveItemsPageView.d.ts +4 -1
  83. package/dist/features/stores/components/StoreLiveItemsPageView.js +16 -12
  84. package/dist/features/stores/components/StorePreOrdersPageView.d.ts +4 -1
  85. package/dist/features/stores/components/StorePreOrdersPageView.js +17 -17
  86. package/dist/features/stores/components/StorePrizeDrawsPageView.d.ts +4 -8
  87. package/dist/features/stores/components/StorePrizeDrawsPageView.js +17 -22
  88. package/dist/features/stores/components/StoreProductsListing.d.ts +7 -1
  89. package/dist/features/stores/components/StoreProductsListing.js +3 -2
  90. package/dist/features/stores/components/StoreProductsPageView.d.ts +1 -1
  91. package/dist/features/stores/components/StoreProductsPageView.js +16 -21
  92. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +602 -0
  93. package/dist/next/routing/route-map.d.ts +2 -0
  94. package/dist/next/routing/route-map.js +5 -0
  95. package/dist/seed/reviews-seed-data.js +52 -0
  96. package/dist/server-entry.d.ts +1 -0
  97. package/dist/server-entry.js +1 -0
  98. package/dist/styles.css +12 -1
  99. package/dist/tailwind-utilities.css +1 -1
  100. package/dist/ui/components/Checkbox.d.ts +10 -1
  101. package/dist/ui/components/Checkbox.js +14 -2
  102. package/dist/ui/components/Checkbox.style.css +11 -0
  103. package/package.json +1 -1
@@ -1,4 +1,8 @@
1
1
  export declare const CHECKOUT_DEFAULT_COMMISSIONS: {
2
+ readonly platformFeePercent: 5;
3
+ readonly gstPercent: 18;
4
+ readonly minimumTransactionFee: 0;
5
+ readonly platformFeeMax: 10;
2
6
  readonly codDepositPercent: 10;
3
7
  readonly codHandlingFeeMin: 200;
4
8
  readonly codHandlingFeePercent: 10;
@@ -1,4 +1,12 @@
1
1
  export const CHECKOUT_DEFAULT_COMMISSIONS = {
2
+ // Platform-commission rates. Previously absent here because no checkout path
3
+ // computed a buyer-facing platform fee — the moment one did, a siteSettings
4
+ // document that failed to load would have produced NaN fees rather than a
5
+ // sane fallback. Mirrors the SiteSettingsDocument defaults.
6
+ platformFeePercent: 5,
7
+ gstPercent: 18,
8
+ minimumTransactionFee: 0,
9
+ platformFeeMax: 10,
2
10
  codDepositPercent: 10,
3
11
  codHandlingFeeMin: 200,
4
12
  codHandlingFeePercent: 10,
@@ -18,17 +18,49 @@ export interface FeeCommissionRates {
18
18
  platformFeePercent: number;
19
19
  gstPercent: number;
20
20
  /** Rupee floor (not paise) — matches the existing `minimumTransactionFee` doc convention. */ minimumTransactionFee?: number;
21
+ /**
22
+ * Rupee ceiling on the buyer-facing platform commission. Optional so
23
+ * siteSettings documents saved before this field existed keep working —
24
+ * falls back to DEFAULT_PLATFORM_FEE_MAX (₹10).
25
+ *
26
+ * Applies to `computeCheckoutFees` (what the BUYER pays) only. The seller-side
27
+ * `computePayoutDeduction` is deliberately uncapped — that is a separate
28
+ * commercial decision, not the same number.
29
+ */ platformFeeMax?: number;
21
30
  gatewayFeePercent?: number;
22
31
  }
23
32
  export interface CheckoutFees {
24
- /** Platform commission in decimal rupees. */
33
+ /** Platform commission in decimal rupees, capped at `platformFeeMax`. */
25
34
  platformFee: number;
26
- /** GST on the platform fee, in decimal rupees. */
35
+ /** GST on the (already capped) platform fee, in decimal rupees. */
27
36
  gstOnFee: number;
28
37
  /** platformFee + gstOnFee, floored at minimumTransactionFee. */
29
38
  totalFee: number;
30
39
  }
40
+ /**
41
+ * What the buyer pays on top of the subtotal.
42
+ *
43
+ * The cap is applied to the commission BEFORE GST is computed, because GST is
44
+ * levied on the commission actually charged — capping afterwards would bill tax
45
+ * on money that was never collected.
46
+ */
31
47
  export declare function computeCheckoutFees(subtotal: number, commissions: FeeCommissionRates): CheckoutFees;
48
+ /**
49
+ * Split one checkout's platform commission across the orders it produces.
50
+ *
51
+ * The buyer is charged a single capped commission for the transaction, but the
52
+ * cart splits into one order per store, and each `OrderDocument` records its own
53
+ * `platformFee` (invoices, reconciliation). Apportioning pro-rata by group
54
+ * subtotal keeps those per-order figures meaningful; giving the last group the
55
+ * rounding remainder guarantees they sum to exactly what was charged, so the
56
+ * orders can never quietly total a rupee more or less than the buyer paid.
57
+ *
58
+ * @param groupSubtotals `[storeId, subtotal]` pairs, in stable order.
59
+ */
60
+ export declare function allocateCheckoutFees(groupSubtotals: readonly (readonly [string, number])[], fees: CheckoutFees): Map<string, {
61
+ platformFee: number;
62
+ gstOnFee: number;
63
+ }>;
32
64
  export interface PayoutDeduction {
33
65
  /** Platform commission in decimal rupees. */
34
66
  platformFee: number;
@@ -15,12 +15,56 @@
15
15
  * All money amounts are in decimal rupees.
16
16
  */
17
17
  import { roundRupees } from "../../../utils/number.formatter";
18
+ /** Buyer-facing platform commission ceiling, in decimal rupees. */
19
+ const DEFAULT_PLATFORM_FEE_MAX = 10;
20
+ /**
21
+ * What the buyer pays on top of the subtotal.
22
+ *
23
+ * The cap is applied to the commission BEFORE GST is computed, because GST is
24
+ * levied on the commission actually charged — capping afterwards would bill tax
25
+ * on money that was never collected.
26
+ */
18
27
  export function computeCheckoutFees(subtotal, commissions) {
19
- const platformFee = roundRupees(subtotal * (commissions.platformFeePercent / 100));
28
+ const uncapped = roundRupees(subtotal * (commissions.platformFeePercent / 100));
29
+ const cap = commissions.platformFeeMax ?? DEFAULT_PLATFORM_FEE_MAX;
30
+ const platformFee = Math.min(uncapped, cap);
20
31
  const gstOnFee = roundRupees(platformFee * (commissions.gstPercent / 100));
21
32
  const totalFee = Math.max(platformFee + gstOnFee, commissions.minimumTransactionFee ?? 0);
22
33
  return { platformFee, gstOnFee, totalFee };
23
34
  }
35
+ /**
36
+ * Split one checkout's platform commission across the orders it produces.
37
+ *
38
+ * The buyer is charged a single capped commission for the transaction, but the
39
+ * cart splits into one order per store, and each `OrderDocument` records its own
40
+ * `platformFee` (invoices, reconciliation). Apportioning pro-rata by group
41
+ * subtotal keeps those per-order figures meaningful; giving the last group the
42
+ * rounding remainder guarantees they sum to exactly what was charged, so the
43
+ * orders can never quietly total a rupee more or less than the buyer paid.
44
+ *
45
+ * @param groupSubtotals `[storeId, subtotal]` pairs, in stable order.
46
+ */
47
+ export function allocateCheckoutFees(groupSubtotals, fees) {
48
+ const allocation = new Map();
49
+ if (groupSubtotals.length === 0)
50
+ return allocation;
51
+ const total = groupSubtotals.reduce((sum, [, subtotal]) => sum + subtotal, 0);
52
+ let platformRemaining = fees.platformFee;
53
+ let gstRemaining = fees.gstOnFee;
54
+ groupSubtotals.forEach(([storeId, subtotal], index) => {
55
+ const isLast = index === groupSubtotals.length - 1;
56
+ // An all-zero cart can't be apportioned by share — fall back to giving the
57
+ // whole (necessarily zero-ish) fee to the last group rather than dividing
58
+ // by zero.
59
+ const share = total > 0 ? subtotal / total : 0;
60
+ const platformFee = isLast ? roundRupees(platformRemaining) : roundRupees(fees.platformFee * share);
61
+ const gstOnFee = isLast ? roundRupees(gstRemaining) : roundRupees(fees.gstOnFee * share);
62
+ platformRemaining = roundRupees(platformRemaining - platformFee);
63
+ gstRemaining = roundRupees(gstRemaining - gstOnFee);
64
+ allocation.set(storeId, { platformFee, gstOnFee });
65
+ });
66
+ return allocation;
67
+ }
24
68
  export function computePayoutDeduction(grossAmount, commissions) {
25
69
  const platformFee = roundRupees(grossAmount * (commissions.platformFeePercent / 100));
26
70
  const gatewayFee = roundRupees(grossAmount * ((commissions.gatewayFeePercent ?? 0) / 100));
package/dist/client.d.ts CHANGED
@@ -207,7 +207,9 @@ export { ProtectedRoute, AuthStatusPanel, ForgotPasswordView, LoginForm, Registe
207
207
  export type { AuthGuardUser, ForgotPasswordViewProps, LoginFormProps, LoginFormValues, RegisterFormProps, RegisterFormValues, ResetPasswordViewProps, VerifyEmailViewProps, } from "./features/auth/index";
208
208
  export { useLogout, useLogin, useGoogleLogin, useLinkGoogleAccount, useRegister, useForgotPassword, useResetPassword, useVerifyEmail, useChangeEmail } from "./features/auth/index";
209
209
  export type { LoginCredentials, RegisterData, ForgotPasswordData, ResetPasswordData, VerifyEmailData, ChangeEmailData } from "./features/auth/index";
210
- export { CartView, CartItemRow, CartSummary, CartDrawer, CheckoutView, CheckoutSuccessView, CheckoutAddressStep, useGuestCart, useCartCount, useAddToCart, useCart, useGuestCartMerge, useCartQuery } from "./features/cart/index";
210
+ export { CartView, CartItemRow, CartSummary, CartPriceBreakdown, CartDrawer, CheckoutView, CheckoutSuccessView, CheckoutAddressStep, useGuestCart, useCartCount, useAddToCart, useCart, useGuestCartMerge, useCartQuery } from "./features/cart/index";
211
+ export { StoreAddonsPicker } from "./features/cart/index";
212
+ export type { CartPriceBreakdownData, CartPriceBreakdownStore, StoreAddonsValue, StoreAddonsRates } from "./features/cart/index";
211
213
  export { getCartOps, CART_OPS_CHANGE_EVENT } from "./features/cart/utils/pending-ops";
212
214
  export type { CartOp } from "./features/cart/utils/pending-ops";
213
215
  export type { CartItem, CartItemMeta, CartData, GuestCartItem } from "./features/cart/index";
@@ -300,6 +302,10 @@ export type { UserAccountHubViewProps, UserAccountHubViewLabels, UserOrdersViewP
300
302
  export { useOrders, useOrder, OrdersList } from "./features/orders/index";
301
303
  export { OrderStatusTimeline } from "./features/orders/components/OrderStatusTimeline";
302
304
  export type { OrderStatusTimelineProps } from "./features/orders/components/OrderStatusTimeline";
305
+ export { CountdownDisplay } from "./ui/components/CountdownDisplay";
306
+ export type { CountdownDisplayProps } from "./ui/components/CountdownDisplay";
307
+ export { OrderAddonBadges } from "./features/orders/components/OrderAddonBadges";
308
+ export type { OrderAddonBadgesOrder, OrderAddonBadgesProps } from "./features/orders/components/OrderAddonBadges";
303
309
  export { MANUAL_PAYMENT_METHODS, isManualPaymentMethod, PAYMENT_REVIEW_QUEUE_MODES, isPaymentReviewQueueMode, PAYMENT_WINDOW_MINUTES, } from "./features/orders/constants/payment-window";
304
310
  export type { PaymentReviewQueueMode } from "./features/orders/constants/payment-window";
305
311
  export { UserOrderTrackView } from "./features/account/components/UserOrderTrackView";
package/dist/client.js CHANGED
@@ -238,7 +238,8 @@ export { PAGE_VIEW_ENTITY_TYPES } from "./features/analytics/types";
238
238
  export { AuctionBidsTable } from "./features/auctions/components/AuctionBidsTable";
239
239
  export { ProtectedRoute, AuthStatusPanel, ForgotPasswordView, LoginForm, RegisterForm, ResetPasswordView, VerifyEmailView } from "./features/auth/index";
240
240
  export { useLogout, useLogin, useGoogleLogin, useLinkGoogleAccount, useRegister, useForgotPassword, useResetPassword, useVerifyEmail, useChangeEmail } from "./features/auth/index";
241
- export { CartView, CartItemRow, CartSummary, CartDrawer, CheckoutView, CheckoutSuccessView, CheckoutAddressStep, useGuestCart, useCartCount, useAddToCart, useCart, useGuestCartMerge, useCartQuery } from "./features/cart/index";
241
+ export { CartView, CartItemRow, CartSummary, CartPriceBreakdown, CartDrawer, CheckoutView, CheckoutSuccessView, CheckoutAddressStep, useGuestCart, useCartCount, useAddToCart, useCart, useGuestCartMerge, useCartQuery } from "./features/cart/index";
242
+ export { StoreAddonsPicker } from "./features/cart/index";
242
243
  export { getCartOps, CART_OPS_CHANGE_EVENT } from "./features/cart/utils/pending-ops";
243
244
  export { useAddresses, useCreateAddress, useUpdateAddress, useDeleteAddress, useSetDefaultAddress, useAddress } from "./features/account/index";
244
245
  export { AddressBook, AddressCard, AddressForm } from "./features/account/index";
@@ -301,6 +302,8 @@ export { SellerPayoutSettingsView, SellerShippingView, SellerReviewsView, Seller
301
302
  export { UserAccountHubView, UserOrdersView, UserBidsView, OrderDetailView, UserNotificationsView, UserReturnsView, UserSupportView, useNotifications } from "./features/account/index";
302
303
  export { useOrders, useOrder, OrdersList } from "./features/orders/index";
303
304
  export { OrderStatusTimeline } from "./features/orders/components/OrderStatusTimeline";
305
+ export { CountdownDisplay } from "./ui/components/CountdownDisplay";
306
+ export { OrderAddonBadges } from "./features/orders/components/OrderAddonBadges";
304
307
  // Manual-payment (cash / UPI / EMI) shared constants — pure data + predicates,
305
308
  // no server deps. Used by the buyer proof-upload page and order-detail CTAs.
306
309
  export { MANUAL_PAYMENT_METHODS, isManualPaymentMethod, PAYMENT_REVIEW_QUEUE_MODES, isPaymentReviewQueueMode, PAYMENT_WINDOW_MINUTES, } from "./features/orders/constants/payment-window";
@@ -234,6 +234,8 @@ export declare const CART_ENDPOINTS: {
234
234
  readonly BY_ITEM_ID: (itemId: string) => string;
235
235
  readonly VALIDATE: "/api/cart/validate";
236
236
  readonly SELECTION: "/api/cart/selection";
237
+ /** Per-store paid add-on selections — the source of truth for what's charged. */
238
+ readonly ADDONS: "/api/cart/addons";
237
239
  readonly COUPON: "/api/cart/coupon";
238
240
  };
239
241
  export declare const CATEGORY_ENDPOINTS: {
@@ -686,6 +688,8 @@ export declare const API_ENDPOINTS: {
686
688
  readonly BY_ITEM_ID: (itemId: string) => string;
687
689
  readonly VALIDATE: "/api/cart/validate";
688
690
  readonly SELECTION: "/api/cart/selection";
691
+ /** Per-store paid add-on selections — the source of truth for what's charged. */
692
+ readonly ADDONS: "/api/cart/addons";
689
693
  readonly COUPON: "/api/cart/coupon";
690
694
  };
691
695
  readonly CATEGORIES: {
@@ -1140,6 +1144,8 @@ export declare const API_ROUTES: {
1140
1144
  readonly BY_ITEM_ID: (itemId: string) => string;
1141
1145
  readonly VALIDATE: "/api/cart/validate";
1142
1146
  readonly SELECTION: "/api/cart/selection";
1147
+ /** Per-store paid add-on selections — the source of truth for what's charged. */
1148
+ readonly ADDONS: "/api/cart/addons";
1143
1149
  readonly COUPON: "/api/cart/coupon";
1144
1150
  };
1145
1151
  readonly CATEGORIES: {
@@ -274,6 +274,8 @@ export const CART_ENDPOINTS = {
274
274
  BY_ITEM_ID: (itemId) => `/api/cart/${encodeURIComponent(itemId)}`,
275
275
  VALIDATE: "/api/cart/validate",
276
276
  SELECTION: "/api/cart/selection",
277
+ /** Per-store paid add-on selections — the source of truth for what's charged. */
278
+ ADDONS: "/api/cart/addons",
277
279
  COUPON: "/api/cart/coupon",
278
280
  };
279
281
  // ---------------------------------------------------------------------------
@@ -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,18 +47,22 @@ 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
67
  // -bannedAt (newest flagged first) to match defaultSort — the option was
64
68
  // "bannedAt" (oldest first), so the dropdown opened with nothing selected.
@@ -121,7 +125,11 @@ function buildAddressConfig(banStatus, onAction) {
121
125
  };
122
126
  }
123
127
  export function AdminAddressesView(_props) {
124
- 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");
125
133
  const queryClient = useQueryClient();
126
134
  const { showToast } = useToast();
127
135
  const actionMutation = useApiMutation({
@@ -139,6 +147,6 @@ export function AdminAddressesView(_props) {
139
147
  const handleAction = (id, action) => {
140
148
  actionMutation.mutate({ id, action });
141
149
  };
142
- const config = buildAddressConfig(banStatus, handleAction);
143
- 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 })] }));
144
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
  }
@@ -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;