@mohasinac/appkit 4.59.0 → 4.60.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (532) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +62 -5
  3. package/dist/_internal/client/features/maintenance/views/MaintenanceDashboardView.js +2 -2
  4. package/dist/_internal/server/features/brands/actions.js +78 -4
  5. package/dist/_internal/server/features/checkout/actions.d.ts +36 -13
  6. package/dist/_internal/server/features/checkout/actions.js +668 -183
  7. package/dist/_internal/server/features/checkout/supplier-identity.js +18 -1
  8. package/dist/_internal/server/features/gateway-events/index.d.ts +10 -0
  9. package/dist/_internal/server/features/gateway-events/index.js +10 -0
  10. package/dist/_internal/server/features/gateway-events/ingest.d.ts +127 -0
  11. package/dist/_internal/server/features/gateway-events/ingest.js +188 -0
  12. package/dist/_internal/server/features/gateway-events/redact.d.ts +66 -0
  13. package/dist/_internal/server/features/gateway-events/redact.js +151 -0
  14. package/dist/_internal/server/features/grievance/actions.d.ts +63 -0
  15. package/dist/_internal/server/features/grievance/actions.js +173 -0
  16. package/dist/_internal/server/features/grievance/index.d.ts +8 -0
  17. package/dist/_internal/server/features/grievance/index.js +8 -0
  18. package/dist/_internal/server/features/grouped/data.js +28 -3
  19. package/dist/_internal/server/features/lottery/actions.js +99 -4
  20. package/dist/_internal/server/features/order-completion/actions.d.ts +92 -0
  21. package/dist/_internal/server/features/order-completion/actions.js +336 -0
  22. package/dist/_internal/server/features/order-completion/index.d.ts +7 -0
  23. package/dist/_internal/server/features/order-completion/index.js +7 -0
  24. package/dist/_internal/server/features/orders/actions.d.ts +13 -0
  25. package/dist/_internal/server/features/orders/actions.js +56 -2
  26. package/dist/_internal/server/features/orders/adapters.d.ts +20 -2
  27. package/dist/_internal/server/features/orders/adapters.js +44 -0
  28. package/dist/_internal/server/features/orders/cancellation-refund.d.ts +116 -0
  29. package/dist/_internal/server/features/orders/cancellation-refund.js +466 -0
  30. package/dist/_internal/server/features/orders/index.d.ts +2 -1
  31. package/dist/_internal/server/features/orders/index.js +1 -1
  32. package/dist/_internal/server/features/orders/service.d.ts +60 -0
  33. package/dist/_internal/server/features/orders/service.js +74 -0
  34. package/dist/_internal/server/features/payment-cases/disputes.d.ts +76 -0
  35. package/dist/_internal/server/features/payment-cases/disputes.js +241 -0
  36. package/dist/_internal/server/features/payment-cases/index.d.ts +9 -0
  37. package/dist/_internal/server/features/payment-cases/index.js +9 -0
  38. package/dist/_internal/server/features/payouts/actions.d.ts +28 -3
  39. package/dist/_internal/server/features/payouts/actions.js +84 -10
  40. package/dist/_internal/server/features/points/adjust.d.ts +61 -0
  41. package/dist/_internal/server/features/points/adjust.js +121 -0
  42. package/dist/_internal/server/features/points/award.d.ts +65 -0
  43. package/dist/_internal/server/features/points/award.js +145 -0
  44. package/dist/_internal/server/features/price-index/market.d.ts +33 -0
  45. package/dist/_internal/server/features/price-index/market.js +54 -1
  46. package/dist/_internal/server/features/products/data.js +40 -22
  47. package/dist/_internal/server/features/protection-claims/actions.d.ts +72 -0
  48. package/dist/_internal/server/features/protection-claims/actions.js +228 -0
  49. package/dist/_internal/server/features/protection-claims/index.d.ts +8 -0
  50. package/dist/_internal/server/features/protection-claims/index.js +8 -0
  51. package/dist/_internal/server/features/refunds/actions.d.ts +20 -0
  52. package/dist/_internal/server/features/refunds/actions.js +259 -17
  53. package/dist/_internal/server/features/refunds/capacity.d.ts +29 -0
  54. package/dist/_internal/server/features/refunds/capacity.js +37 -0
  55. package/dist/_internal/server/features/reviews/actions.js +3 -2
  56. package/dist/_internal/server/features/search/index-build/sources.js +20 -1
  57. package/dist/_internal/server/features/shipping/reconcile.d.ts +10 -1
  58. package/dist/_internal/server/features/shipping/reconcile.js +34 -3
  59. package/dist/_internal/server/features/shipping/reimbursement.d.ts +117 -0
  60. package/dist/_internal/server/features/shipping/reimbursement.js +223 -0
  61. package/dist/_internal/server/features/stores/tabs.d.ts +19 -4
  62. package/dist/_internal/server/features/stores/tabs.js +24 -4
  63. package/dist/_internal/server/functions/https.d.ts +31 -1
  64. package/dist/_internal/server/functions/https.js +52 -1
  65. package/dist/_internal/server/functions/scheduled.d.ts +72 -1
  66. package/dist/_internal/server/functions/scheduled.js +124 -1
  67. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +62 -16
  68. package/dist/_internal/server/jobs/core/boxRollupReconcile.d.ts +46 -0
  69. package/dist/_internal/server/jobs/core/boxRollupReconcile.js +142 -0
  70. package/dist/_internal/server/jobs/core/dailyDataCleanup.js +149 -1
  71. package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +14 -0
  72. package/dist/_internal/server/jobs/core/dailyStatusDigest.js +79 -1
  73. package/dist/_internal/server/jobs/core/index.d.ts +2 -0
  74. package/dist/_internal/server/jobs/core/index.js +2 -0
  75. package/dist/_internal/server/jobs/core/marketPriceRollup.d.ts +64 -0
  76. package/dist/_internal/server/jobs/core/marketPriceRollup.js +153 -0
  77. package/dist/_internal/server/jobs/core/obligationSweep.d.ts +36 -0
  78. package/dist/_internal/server/jobs/core/obligationSweep.js +503 -0
  79. package/dist/_internal/server/jobs/core/paymentReconcile.d.ts +79 -0
  80. package/dist/_internal/server/jobs/core/paymentReconcile.js +614 -0
  81. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +34 -0
  82. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +35 -4
  83. package/dist/_internal/server/jobs/handlers/boxRollupReconcile.d.ts +2 -0
  84. package/dist/_internal/server/jobs/handlers/boxRollupReconcile.js +15 -0
  85. package/dist/_internal/server/jobs/handlers/countersReconcileNow.d.ts +65 -0
  86. package/dist/_internal/server/jobs/handlers/countersReconcileNow.js +89 -0
  87. package/dist/_internal/server/jobs/handlers/index.d.ts +5 -0
  88. package/dist/_internal/server/jobs/handlers/index.js +5 -0
  89. package/dist/_internal/server/jobs/handlers/marketPriceRollup.d.ts +2 -0
  90. package/dist/_internal/server/jobs/handlers/marketPriceRollup.js +2 -0
  91. package/dist/_internal/server/jobs/handlers/obligationSweep.d.ts +2 -0
  92. package/dist/_internal/server/jobs/handlers/obligationSweep.js +15 -0
  93. package/dist/_internal/server/jobs/handlers/onShipmentAllocationSync.js +30 -1
  94. package/dist/_internal/server/jobs/handlers/paymentReconcile.d.ts +2 -0
  95. package/dist/_internal/server/jobs/handlers/paymentReconcile.js +6 -0
  96. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  97. package/dist/_internal/shared/actions/action-registry.js +71 -0
  98. package/dist/_internal/shared/checkout/coupon-discount.d.ts +73 -0
  99. package/dist/_internal/shared/checkout/coupon-discount.js +103 -0
  100. package/dist/_internal/shared/checkout/lanes.d.ts +42 -0
  101. package/dist/_internal/shared/checkout/lanes.js +46 -0
  102. package/dist/_internal/shared/checkout/order-math.d.ts +149 -1
  103. package/dist/_internal/shared/checkout/order-math.js +251 -3
  104. package/dist/_internal/shared/checkout/refundable-base.d.ts +116 -0
  105. package/dist/_internal/shared/checkout/refundable-base.js +122 -0
  106. package/dist/_internal/shared/features/auctions/config.d.ts +14 -1
  107. package/dist/_internal/shared/features/auctions/config.js +30 -1
  108. package/dist/_internal/shared/features/brands/schema.d.ts +42 -0
  109. package/dist/_internal/shared/features/brands/schema.js +18 -0
  110. package/dist/_internal/shared/features/orders/refund-copy.d.ts +22 -0
  111. package/dist/_internal/shared/features/orders/refund-copy.js +34 -0
  112. package/dist/_internal/shared/features/print/label-profiles.d.ts +198 -0
  113. package/dist/_internal/shared/features/print/label-profiles.js +350 -0
  114. package/dist/_internal/shared/features/products/to-product-item.js +33 -5
  115. package/dist/_internal/shared/fees/calculator.d.ts +31 -2
  116. package/dist/_internal/shared/fees/calculator.js +103 -5
  117. package/dist/_internal/shared/fees/india-states.d.ts +195 -0
  118. package/dist/_internal/shared/fees/india-states.js +190 -0
  119. package/dist/_internal/shared/fees/payout-hold.d.ts +80 -0
  120. package/dist/_internal/shared/fees/payout-hold.js +74 -0
  121. package/dist/_internal/shared/obligations/deadlines.d.ts +94 -0
  122. package/dist/_internal/shared/obligations/deadlines.js +155 -0
  123. package/dist/_internal/shared/shipping/quote.d.ts +96 -0
  124. package/dist/_internal/shared/shipping/quote.js +67 -0
  125. package/dist/_internal/shared/time/business-days.d.ts +91 -0
  126. package/dist/_internal/shared/time/business-days.js +185 -0
  127. package/dist/cli/index.js +0 -2
  128. package/dist/client.d.ts +10 -4
  129. package/dist/client.js +29 -3
  130. package/dist/configs/tailwind.d.ts +0 -12
  131. package/dist/configs/tailwind.js +28 -15
  132. package/dist/constants/api-endpoints.d.ts +51 -9
  133. package/dist/constants/api-endpoints.js +23 -7
  134. package/dist/constants/index.d.ts +1 -1
  135. package/dist/constants/index.js +1 -1
  136. package/dist/contracts/index.d.ts +1 -1
  137. package/dist/contracts/payment.d.ts +131 -0
  138. package/dist/features/about/components/AboutView.js +1 -1
  139. package/dist/features/about/components/DeveloperView.js +5 -2
  140. package/dist/features/about/components/FAQPageView.js +2 -1
  141. package/dist/features/about/components/FeesView.js +2 -1
  142. package/dist/features/about/components/HelpPageView.js +2 -1
  143. package/dist/features/about/components/HowAuctionsWorkView.js +2 -1
  144. package/dist/features/about/components/HowCheckoutWorksView.js +2 -1
  145. package/dist/features/about/components/HowEmiWorksView.js +2 -1
  146. package/dist/features/about/components/HowOffersWorkView.js +2 -1
  147. package/dist/features/about/components/HowOrdersWorkView.js +2 -1
  148. package/dist/features/about/components/HowPayoutsWorkView.js +2 -1
  149. package/dist/features/about/components/HowPreOrdersWorkView.js +2 -1
  150. package/dist/features/about/components/HowReviewsWorkView.js +2 -1
  151. package/dist/features/about/components/PolicyPageView.js +2 -1
  152. package/dist/features/about/components/ProseHero.d.ts +73 -0
  153. package/dist/features/about/components/ProseHero.js +18 -0
  154. package/dist/features/about/components/SecurityPrivacyView.js +2 -1
  155. package/dist/features/about/components/ShippingPolicyView.js +2 -1
  156. package/dist/features/about/components/TrackOrderView.js +2 -1
  157. package/dist/features/about/components/UnauthorizedView.js +1 -1
  158. package/dist/features/about/index.d.ts +2 -0
  159. package/dist/features/about/index.js +5 -0
  160. package/dist/features/account/actions/address-actions.d.ts +26 -2
  161. package/dist/features/account/actions/address-actions.js +25 -1
  162. package/dist/features/account/components/BuyerShoppingGuideView.js +1 -1
  163. package/dist/features/account/components/NotificationPreferencesPanel.js +1 -1
  164. package/dist/features/account/components/UserOrdersView.js +17 -0
  165. package/dist/features/account/components/UserSidebar.js +1 -1
  166. package/dist/features/account/hooks/useNotifications.js +29 -0
  167. package/dist/features/admin/actions/admin-coupon-actions.js +36 -0
  168. package/dist/features/admin/components/AdminBeysView.js +2 -2
  169. package/dist/features/admin/components/AdminBlogEditorView.js +2 -2
  170. package/dist/features/admin/components/AdminBrandEditorView.js +32 -2
  171. package/dist/features/admin/components/AdminBundleEditorView.js +1 -1
  172. package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
  173. package/dist/features/admin/components/AdminCategoriesView.js +1 -1
  174. package/dist/features/admin/components/AdminCategoryEditorView.js +73 -17
  175. package/dist/features/admin/components/AdminContactEditorView.js +1 -1
  176. package/dist/features/admin/components/AdminCouponEditorView.js +26 -1
  177. package/dist/features/admin/components/AdminFaqEditorView.js +1 -1
  178. package/dist/features/admin/components/AdminFaqsView.js +2 -2
  179. package/dist/features/admin/components/AdminGatewayEventsView.d.ts +54 -0
  180. package/dist/features/admin/components/AdminGatewayEventsView.js +209 -0
  181. package/dist/features/admin/components/AdminGuideHubView.js +1 -1
  182. package/dist/features/admin/components/AdminMoneyCasesView.d.ts +60 -0
  183. package/dist/features/admin/components/AdminMoneyCasesView.js +287 -0
  184. package/dist/features/admin/components/AdminPartsView.js +2 -2
  185. package/dist/features/admin/components/AdminProductsView.js +75 -4
  186. package/dist/features/admin/components/AdminSidebar.js +2 -2
  187. package/dist/features/admin/components/AdminSiteSettingsView.js +65 -7
  188. package/dist/features/admin/components/AdminSublistingCategoriesView.js +2 -0
  189. package/dist/features/admin/components/AdminSupportTicketDetailView.js +1 -1
  190. package/dist/features/admin/components/AdminSupportTicketsView.js +2 -2
  191. package/dist/features/admin/components/AdminTesterChecklistView.js +2 -2
  192. package/dist/features/admin/components/AdminUsersView.js +72 -1
  193. package/dist/features/admin/components/AdminViewCards.d.ts +3 -1
  194. package/dist/features/admin/components/AdminViewCards.js +4 -4
  195. package/dist/features/admin/components/DataListingView.d.ts +28 -0
  196. package/dist/features/admin/components/DataListingView.js +175 -14
  197. package/dist/features/admin/components/DataTable.d.ts +12 -1
  198. package/dist/features/admin/components/DataTable.js +3 -3
  199. package/dist/features/admin/components/OwnerQuickActions.d.ts +1 -1
  200. package/dist/features/admin/components/OwnerQuickActions.js +60 -2
  201. package/dist/features/admin/components/RecordOwnerQuickActions.d.ts +1 -1
  202. package/dist/features/admin/components/RecordOwnerQuickActions.js +61 -14
  203. package/dist/features/admin/components/analytics/AdminStatCard.d.ts +35 -1
  204. package/dist/features/admin/components/analytics/AdminStatCard.js +77 -2
  205. package/dist/features/admin/components/index.d.ts +4 -0
  206. package/dist/features/admin/components/index.js +6 -0
  207. package/dist/features/admin/constants/filter-tabs.d.ts +156 -0
  208. package/dist/features/admin/constants/filter-tabs.js +106 -0
  209. package/dist/features/admin/repository/site-settings.repository.d.ts +22 -0
  210. package/dist/features/admin/repository/site-settings.repository.js +82 -1
  211. package/dist/features/admin/schemas/admin-editor-forms.d.ts +21 -0
  212. package/dist/features/admin/schemas/admin-editor-forms.js +44 -0
  213. package/dist/features/admin/schemas/firestore.js +5 -1
  214. package/dist/features/admin/schemas/site-settings-form.js +30 -1
  215. package/dist/features/auctions/actions/bid-actions.js +13 -1
  216. package/dist/features/auctions/components/AuctionBottomActions.js +2 -2
  217. package/dist/features/auctions/components/AuctionDetailPageView.js +3 -1
  218. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +11 -0
  219. package/dist/features/auctions/components/MarketplaceAuctionCard.js +5 -3
  220. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +8 -1
  221. package/dist/features/auctions/components/PlaceBidFormClient.js +2 -2
  222. package/dist/features/audit-log/schemas/firestore.d.ts +28 -0
  223. package/dist/features/audit-log/schemas/firestore.js +14 -0
  224. package/dist/features/auth/permissions/constants.d.ts +17 -1
  225. package/dist/features/auth/permissions/constants.js +18 -0
  226. package/dist/features/blog/components/BlogPostView.js +1 -1
  227. package/dist/features/catalogue/components/AdminCatalogueApprovalsView.js +1 -1
  228. package/dist/features/categories/api/[id]/description-template/route.js +107 -1
  229. package/dist/features/categories/components/BrandDetailPageView.js +2 -2
  230. package/dist/features/categories/components/BreadcrumbTrail.js +7 -1
  231. package/dist/features/categories/components/BundleDetailView.js +1 -1
  232. package/dist/features/categories/components/CategoryDetailPageView.js +2 -2
  233. package/dist/features/categories/components/CategoryForm.js +12 -1
  234. package/dist/features/categories/components/CategoryTemplatePicker.js +38 -5
  235. package/dist/features/categories/repository/market-price-index.repository.d.ts +123 -0
  236. package/dist/features/categories/repository/market-price-index.repository.js +105 -0
  237. package/dist/features/categories/repository/price-observation.repository.d.ts +166 -0
  238. package/dist/features/categories/repository/price-observation.repository.js +132 -0
  239. package/dist/features/events/components/EventSaleBanner.js +1 -1
  240. package/dist/features/events/components/EventStatusBadge.js +1 -1
  241. package/dist/features/faq/components/FAQAccordion.js +2 -1
  242. package/dist/features/faq/components/FAQCategorySidebar.js +2 -2
  243. package/dist/features/faq/components/FAQHelpfulButtons.js +4 -4
  244. package/dist/features/faq/components/RelatedFAQs.js +1 -1
  245. package/dist/features/filters/FilterFacetSection.js +1 -1
  246. package/dist/features/filters/SwitchFilter.js +1 -1
  247. package/dist/features/gateway-events/repository/gateway-events.repository.d.ts +117 -0
  248. package/dist/features/gateway-events/repository/gateway-events.repository.js +274 -0
  249. package/dist/features/gateway-events/schemas/firestore.d.ts +217 -0
  250. package/dist/features/gateway-events/schemas/firestore.js +125 -0
  251. package/dist/features/gateway-events/schemas/index.d.ts +81 -0
  252. package/dist/features/gateway-events/schemas/index.js +79 -0
  253. package/dist/features/grouped/components/GroupedListingDetailView.js +1 -1
  254. package/dist/features/homepage/components/AnnouncementBar.js +37 -1
  255. package/dist/features/homepage/components/AnnouncementBar.style.css +30 -0
  256. package/dist/features/homepage/components/BrandsSection.js +1 -1
  257. package/dist/features/homepage/components/CTABannerSection.js +1 -1
  258. package/dist/features/homepage/components/FeaturedBundlesSection.js +14 -24
  259. package/dist/features/homepage/components/HeroCarousel.js +2 -2
  260. package/dist/features/homepage/components/SocialPostCard.js +1 -1
  261. package/dist/features/inventory/components/AdminMoveLedgerView.d.ts +7 -0
  262. package/dist/features/inventory/components/AdminMoveLedgerView.js +157 -0
  263. package/dist/features/inventory/components/AdminStorageBoxesView.d.ts +11 -0
  264. package/dist/features/inventory/components/AdminStorageBoxesView.js +168 -0
  265. package/dist/features/inventory/components/ReceiveStationView.d.ts +39 -0
  266. package/dist/features/inventory/components/ReceiveStationView.js +413 -0
  267. package/dist/features/inventory/components/operator-message.d.ts +29 -0
  268. package/dist/features/inventory/components/operator-message.js +41 -0
  269. package/dist/features/inventory/index.d.ts +13 -0
  270. package/dist/features/inventory/index.js +28 -0
  271. package/dist/features/inventory/repository/inventory-movements.repository.d.ts +97 -0
  272. package/dist/features/inventory/repository/inventory-movements.repository.js +186 -0
  273. package/dist/features/inventory/repository/inventory-placements.repository.d.ts +179 -0
  274. package/dist/features/inventory/repository/inventory-placements.repository.js +345 -0
  275. package/dist/features/inventory/repository/storage-boxes.repository.d.ts +83 -0
  276. package/dist/features/inventory/repository/storage-boxes.repository.js +127 -0
  277. package/dist/features/inventory/schemas/firestore.d.ts +197 -0
  278. package/dist/features/inventory/schemas/firestore.js +159 -0
  279. package/dist/features/layout/AppLayoutShell.d.ts +12 -1
  280. package/dist/features/layout/AppLayoutShell.js +25 -12
  281. package/dist/features/layout/AutoBreadcrumbs.js +162 -3
  282. package/dist/features/layout/AutoBreadcrumbs.style.css +45 -0
  283. package/dist/features/layout/BottomNavbar.d.ts +1 -1
  284. package/dist/features/layout/BottomNavbar.js +2 -2
  285. package/dist/features/media/MediaAvatar.js +1 -1
  286. package/dist/features/media/MediaImage.d.ts +24 -4
  287. package/dist/features/media/MediaImage.js +44 -4
  288. package/dist/features/media/MediaLightbox.js +1 -1
  289. package/dist/features/media/modals/ImageCropModal.js +1 -1
  290. package/dist/features/media/modals/ImageEditor.js +1 -1
  291. package/dist/features/media/upload/MediaUploadField.js +91 -3
  292. package/dist/features/obligations/constants/policies.d.ts +92 -0
  293. package/dist/features/obligations/constants/policies.js +378 -0
  294. package/dist/features/obligations/repository/obligations.repository.d.ts +68 -0
  295. package/dist/features/obligations/repository/obligations.repository.js +158 -0
  296. package/dist/features/obligations/schemas/firestore.d.ts +160 -0
  297. package/dist/features/obligations/schemas/firestore.js +87 -0
  298. package/dist/features/orders/actions/order-actions.d.ts +43 -4
  299. package/dist/features/orders/actions/order-actions.js +60 -3
  300. package/dist/features/orders/actions/refund-actions.js +70 -10
  301. package/dist/features/orders/components/OrderCompletionPanel.d.ts +27 -0
  302. package/dist/features/orders/components/OrderCompletionPanel.js +192 -0
  303. package/dist/features/orders/components/OrdersList.js +2 -2
  304. package/dist/features/orders/components/RefundHistoryTable.js +3 -1
  305. package/dist/features/orders/components/index.d.ts +2 -0
  306. package/dist/features/orders/components/index.js +6 -0
  307. package/dist/features/orders/constants/transitions.d.ts +59 -0
  308. package/dist/features/orders/constants/transitions.js +104 -0
  309. package/dist/features/orders/repository/orders.repository.d.ts +109 -6
  310. package/dist/features/orders/repository/orders.repository.js +464 -21
  311. package/dist/features/orders/schemas/firestore.d.ts +270 -0
  312. package/dist/features/orders/schemas/firestore.js +24 -0
  313. package/dist/features/orders/schemas/index.d.ts +61 -0
  314. package/dist/features/orders/schemas/index.js +22 -0
  315. package/dist/features/orders/types/index.d.ts +79 -0
  316. package/dist/features/parts/components/PartsIndexPageView.js +8 -1
  317. package/dist/features/payment-cases/repository/payment-cases.repository.d.ts +170 -0
  318. package/dist/features/payment-cases/repository/payment-cases.repository.js +342 -0
  319. package/dist/features/payment-cases/schemas/firestore.d.ts +229 -0
  320. package/dist/features/payment-cases/schemas/firestore.js +105 -0
  321. package/dist/features/payment-cases/schemas/resolve-form.d.ts +97 -0
  322. package/dist/features/payment-cases/schemas/resolve-form.js +114 -0
  323. package/dist/features/payments/repository/payout.repository.d.ts +0 -1
  324. package/dist/features/payments/repository/payout.repository.js +0 -1
  325. package/dist/features/points/repository/points.repository.d.ts +122 -0
  326. package/dist/features/points/repository/points.repository.js +304 -1
  327. package/dist/features/points/schemas/adjust-form.d.ts +58 -0
  328. package/dist/features/points/schemas/adjust-form.js +63 -0
  329. package/dist/features/points/schemas/firestore.d.ts +108 -1
  330. package/dist/features/points/schemas/firestore.js +84 -0
  331. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -3
  332. package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
  333. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
  334. package/dist/features/price-index/components/PriceIndexToolView.d.ts +23 -4
  335. package/dist/features/price-index/components/PriceIndexToolView.js +71 -6
  336. package/dist/features/price-index/market-stats.d.ts +82 -0
  337. package/dist/features/price-index/market-stats.js +230 -0
  338. package/dist/features/print/components/BoxLabelPrintView.d.ts +25 -0
  339. package/dist/features/print/components/BoxLabelPrintView.js +66 -0
  340. package/dist/features/print/components/LabelProfilePicker.d.ts +13 -0
  341. package/dist/features/print/components/LabelProfilePicker.js +38 -0
  342. package/dist/features/print/components/labels.d.ts +117 -0
  343. package/dist/features/print/components/labels.js +258 -0
  344. package/dist/features/print/components/labels.style.css +46 -0
  345. package/dist/features/print/index.d.ts +6 -0
  346. package/dist/features/print/index.js +16 -0
  347. package/dist/features/products/columns/productTableColumns.js +5 -4
  348. package/dist/features/products/components/MarketplaceBundleCard.js +2 -2
  349. package/dist/features/products/components/MarketplacePrizeDrawCard.js +6 -6
  350. package/dist/features/products/components/PrizeDrawCollage.js +1 -1
  351. package/dist/features/products/components/PrizeDrawItemsEditor.js +1 -1
  352. package/dist/features/products/components/PrizeRevealModal.js +2 -2
  353. package/dist/features/products/components/ProductDetailPageView.js +18 -4
  354. package/dist/features/products/components/ProductForm.js +34 -23
  355. package/dist/features/products/components/ProductGrid.js +86 -62
  356. package/dist/features/products/constants/card-badges.d.ts +97 -0
  357. package/dist/features/products/constants/card-badges.js +100 -0
  358. package/dist/features/products/repository/products.repository.js +34 -1
  359. package/dist/features/products/schemas/firestore.d.ts +46 -3
  360. package/dist/features/products/schemas/firestore.js +21 -0
  361. package/dist/features/promotions/components/PromotionsView.js +1 -1
  362. package/dist/features/promotions/repository/claimed-coupons.repository.d.ts +32 -0
  363. package/dist/features/promotions/repository/claimed-coupons.repository.js +58 -0
  364. package/dist/features/promotions/schemas/firestore.js +30 -3
  365. package/dist/features/promotions/schemas/index.d.ts +6 -6
  366. package/dist/features/protection-claims/repository/protection-claims.repository.d.ts +54 -0
  367. package/dist/features/protection-claims/repository/protection-claims.repository.js +174 -0
  368. package/dist/features/protection-claims/schemas/firestore.d.ts +168 -0
  369. package/dist/features/protection-claims/schemas/firestore.js +130 -0
  370. package/dist/features/reviews/actions/review-actions.js +3 -2
  371. package/dist/features/reviews/components/ReviewModal.js +1 -1
  372. package/dist/features/reviews/components/ReviewsList.js +1 -1
  373. package/dist/features/reviews/schemas/firestore.d.ts +20 -1
  374. package/dist/features/scams/constants/scam-types.d.ts +2 -0
  375. package/dist/features/scams/constants/scam-types.js +18 -0
  376. package/dist/features/scams/schemas/report-form.d.ts +3 -3
  377. package/dist/features/scams/schemas/report-form.js +26 -1
  378. package/dist/features/search/components/Search.js +9 -9
  379. package/dist/features/search/components/SearchResultsClient.js +18 -3
  380. package/dist/features/search/components/SearchResultsPageView.d.ts +1 -6
  381. package/dist/features/search/components/SearchResultsPageView.js +2 -2
  382. package/dist/features/seller/actions/offer-actions.js +16 -1
  383. package/dist/features/seller/actions/seller-actions.js +14 -3
  384. package/dist/features/seller/components/CategoryInlineSelect.js +75 -11
  385. package/dist/features/seller/components/PrintCenterView.d.ts +5 -0
  386. package/dist/features/seller/components/PrintCenterView.js +41 -30
  387. package/dist/features/seller/components/SellerOrdersView.js +1 -1
  388. package/dist/features/seller/components/SellerPayoutRequestView.js +16 -5
  389. package/dist/features/seller/components/SellerPayoutSettingsView.js +1 -1
  390. package/dist/features/seller/components/SellerProductShell.d.ts +16 -0
  391. package/dist/features/seller/components/SellerProductShell.js +123 -13
  392. package/dist/features/seller/components/SellerReviewsView.js +4 -2
  393. package/dist/features/seller/components/SellerSidebar.js +1 -1
  394. package/dist/features/seller/schemas/coupon-form.d.ts +26 -0
  395. package/dist/features/seller/schemas/coupon-form.js +27 -0
  396. package/dist/features/seller/schemas/product-form.d.ts +3 -0
  397. package/dist/features/seller/schemas/product-form.js +5 -0
  398. package/dist/features/shell/FormShell.js +2 -2
  399. package/dist/features/shell/QuickFormDrawer.js +14 -0
  400. package/dist/features/shell/SectionForm.d.ts +19 -0
  401. package/dist/features/shell/SectionForm.js +133 -9
  402. package/dist/features/shell/SectionForm.style.css +54 -0
  403. package/dist/features/shipments/components/AdminShipmentsView.js +16 -1
  404. package/dist/features/shipments/repository/shipment-items.repository.d.ts +9 -0
  405. package/dist/features/shipments/repository/shipment-items.repository.js +14 -0
  406. package/dist/features/site-settings/components/ActionPermissionsManager.js +1 -1
  407. package/dist/features/sticker-trace/components/StickerCommitPanel.js +1 -1
  408. package/dist/features/store-extensions/schemas/report-review-form.d.ts +8 -8
  409. package/dist/features/stores/components/StoreGuideHubView.js +1 -1
  410. package/dist/features/stores/components/StoreHeader.js +1 -1
  411. package/dist/features/support/schemas/firestore.d.ts +73 -2
  412. package/dist/features/support/schemas/firestore.js +59 -0
  413. package/dist/index.d.ts +16 -24
  414. package/dist/index.js +38 -47
  415. package/dist/next/ErrorBoundary.js +1 -1
  416. package/dist/next/routing/route-map.d.ts +24 -0
  417. package/dist/next/routing/route-map.js +40 -0
  418. package/dist/providers/payment-manual/index.d.ts +15 -1
  419. package/dist/providers/payment-manual/index.js +37 -0
  420. package/dist/providers/payment-phonepe/index.d.ts +14 -1
  421. package/dist/providers/payment-phonepe/index.js +36 -0
  422. package/dist/providers/payment-razorpay/index.d.ts +44 -1
  423. package/dist/providers/payment-razorpay/index.js +99 -0
  424. package/dist/providers/shipping-shiprocket/index.d.ts +18 -0
  425. package/dist/providers/shipping-shiprocket/index.js +18 -0
  426. package/dist/schemas/registry.d.ts +44 -11
  427. package/dist/server-entry.d.ts +2 -0
  428. package/dist/server-entry.js +27 -0
  429. package/dist/server.d.ts +43 -0
  430. package/dist/server.js +98 -0
  431. package/dist/styles.css +418 -149
  432. package/dist/tailwind-utilities.css +1 -1
  433. package/dist/tokens/themes/default-dark.js +15 -12
  434. package/dist/tokens/themes/default-light.js +14 -11
  435. package/dist/tokens/tokens.css +106 -23
  436. package/dist/ui/DataTable.style.css +4 -4
  437. package/dist/ui/columns/cell-renderers.d.ts +20 -0
  438. package/dist/ui/columns/cell-renderers.js +26 -1
  439. package/dist/ui/columns/index.d.ts +1 -1
  440. package/dist/ui/columns/index.js +4 -1
  441. package/dist/ui/components/Badge.style.css +1 -1
  442. package/dist/ui/components/Breadcrumb.js +19 -1
  443. package/dist/ui/components/BulkActionBar.style.css +4 -4
  444. package/dist/ui/components/Button.style.css +1 -1
  445. package/dist/ui/components/Checkbox.style.css +1 -1
  446. package/dist/ui/components/FallbackShell.js +1 -1
  447. package/dist/ui/components/Form.d.ts +14 -1
  448. package/dist/ui/components/Form.js +2 -2
  449. package/dist/ui/components/FormField.js +18 -4
  450. package/dist/ui/components/FormField.style.css +43 -3
  451. package/dist/ui/components/GatedPrice.style.css +1 -1
  452. package/dist/ui/components/IconButton.style.css +1 -1
  453. package/dist/ui/components/Input.js +22 -1
  454. package/dist/ui/components/Input.style.css +38 -22
  455. package/dist/ui/components/ListingFilterDrawer.js +1 -1
  456. package/dist/ui/components/ListingToolbar.js +1 -1
  457. package/dist/ui/components/PaginatedSelect.js +0 -0
  458. package/dist/ui/components/Radio.style.css +1 -1
  459. package/dist/ui/components/Repeater.d.ts +53 -0
  460. package/dist/ui/components/Repeater.js +7 -0
  461. package/dist/ui/components/RichTextEditor.d.ts +20 -1
  462. package/dist/ui/components/RichTextEditor.js +23 -2
  463. package/dist/ui/components/Select.style.css +7 -21
  464. package/dist/ui/components/Semantic.js +27 -3
  465. package/dist/ui/components/TagInput.style.css +4 -9
  466. package/dist/ui/components/Textarea.style.css +4 -18
  467. package/dist/ui/components/Toggle.style.css +1 -1
  468. package/dist/ui/components/Typography.d.ts +3 -3
  469. package/dist/ui/components/Typography.js +15 -15
  470. package/dist/ui/components/ViewToggle.style.css +4 -4
  471. package/dist/ui/forms/AddAnotherToggle.d.ts +16 -0
  472. package/dist/ui/forms/AddAnotherToggle.js +56 -0
  473. package/dist/ui/forms/ColorPickerField.js +15 -2
  474. package/dist/ui/forms/FieldCheckbox.js +15 -2
  475. package/dist/ui/forms/FieldInput.d.ts +22 -1
  476. package/dist/ui/forms/FieldInput.js +19 -5
  477. package/dist/ui/forms/FieldLabel.d.ts +35 -0
  478. package/dist/ui/forms/FieldLabel.js +70 -0
  479. package/dist/ui/forms/FieldSelect.d.ts +4 -1
  480. package/dist/ui/forms/FieldSelect.js +27 -5
  481. package/dist/ui/forms/FieldTextarea.d.ts +4 -1
  482. package/dist/ui/forms/FieldTextarea.js +22 -5
  483. package/dist/ui/forms/FormShell.d.ts +17 -1
  484. package/dist/ui/forms/FormShell.js +36 -2
  485. package/dist/ui/forms/FormShell.style.css +41 -33
  486. package/dist/ui/forms/index.d.ts +5 -1
  487. package/dist/ui/forms/index.js +2 -0
  488. package/dist/ui/icons/icon-registry.d.ts +1 -0
  489. package/dist/ui/icons/icon-registry.js +17 -1
  490. package/dist/utils/id-generators.d.ts +80 -0
  491. package/dist/utils/id-generators.js +93 -0
  492. package/dist/utils/search-txt-builders.d.ts +12 -0
  493. package/dist/utils/search-txt-builders.js +17 -0
  494. package/package.json +1 -1
  495. package/scripts/seed-cli.mjs +106 -5
  496. package/dist/features/loyalty/api/admin/loyalty/grant/route.d.ts +0 -12
  497. package/dist/features/loyalty/api/admin/loyalty/grant/route.js +0 -38
  498. package/dist/features/loyalty/api/balance/route.d.ts +0 -12
  499. package/dist/features/loyalty/api/balance/route.js +0 -41
  500. package/dist/features/loyalty/api/earn/route.d.ts +0 -12
  501. package/dist/features/loyalty/api/earn/route.js +0 -38
  502. package/dist/features/loyalty/api/history/route.d.ts +0 -12
  503. package/dist/features/loyalty/api/history/route.js +0 -58
  504. package/dist/features/loyalty/api/redeem/route.d.ts +0 -12
  505. package/dist/features/loyalty/api/redeem/route.js +0 -38
  506. package/dist/features/loyalty/columns/index.d.ts +0 -17
  507. package/dist/features/loyalty/columns/index.js +0 -40
  508. package/dist/features/loyalty/components/CoinsDisplay.d.ts +0 -12
  509. package/dist/features/loyalty/components/CoinsDisplay.js +0 -10
  510. package/dist/features/loyalty/components/index.d.ts +0 -1
  511. package/dist/features/loyalty/components/index.js +0 -1
  512. package/dist/features/loyalty/hooks/useLoyaltyBalance.d.ts +0 -4
  513. package/dist/features/loyalty/hooks/useLoyaltyBalance.js +0 -25
  514. package/dist/features/loyalty/index.d.ts +0 -6
  515. package/dist/features/loyalty/index.js +0 -6
  516. package/dist/features/loyalty/manifest.d.ts +0 -2
  517. package/dist/features/loyalty/manifest.js +0 -16
  518. package/dist/features/loyalty/messages/en.json +0 -19
  519. package/dist/features/loyalty/repository/loyalty.repository.d.ts +0 -8
  520. package/dist/features/loyalty/repository/loyalty.repository.js +0 -22
  521. package/dist/features/loyalty/schemas/index.d.ts +0 -85
  522. package/dist/features/loyalty/schemas/index.js +0 -40
  523. package/dist/features/loyalty/server.d.ts +0 -6
  524. package/dist/features/loyalty/server.js +0 -6
  525. package/dist/features/loyalty/types/index.d.ts +0 -31
  526. package/dist/features/loyalty/types/index.js +0 -1
  527. package/dist/features/loyalty/types/loyalty-config.d.ts +0 -7
  528. package/dist/features/loyalty/types/loyalty-config.js +0 -1
  529. package/dist/features/loyalty/types/loyalty-math.d.ts +0 -9
  530. package/dist/features/loyalty/types/loyalty-math.js +0 -27
  531. package/dist/features/products/components/ProductOwnerQuickActions.d.ts +0 -11
  532. package/dist/features/products/components/ProductOwnerQuickActions.js +0 -120
@@ -25,7 +25,7 @@ const SECTIONS = [
25
25
  },
26
26
  {
27
27
  Icon: CheckCircle, title: "Order Confirmation",
28
- content: (_jsxs(_Fragment, { children: [_jsx(Text, { className: GC.textMuted, children: "After a successful payment you'll see an on-screen confirmation with your order ID. You'll also receive:" }), _jsxs(Ul, { className: `${GC.listDiscMuted} mt-3`, children: [_jsx(Li, { children: "An email confirmation to your registered address" }), _jsx(Li, { children: "A WhatsApp message if you've added and verified your phone number" })] }), _jsxs(Text, { className: `${GC.textMuted} mt-3`, children: ["View all your orders at any time under ", _jsx(Span, { weight: "bold", children: "My Account \u2192 My Orders" }), "."] })] })),
28
+ content: (_jsxs(_Fragment, { children: [_jsx(Text, { className: GC.textMuted, children: "After a successful payment you'll see an on-screen confirmation with your order ID. You'll also receive:" }), _jsxs(Ul, { className: `${GC.listDiscMuted} mt-3`, children: [_jsx(Li, { children: "An email confirmation to your registered address" }), _jsx(Li, { children: "An in-app notification you can read any time from the bell in the header" })] }), _jsxs(Text, { className: `${GC.textMuted} mt-3`, children: ["View all your orders at any time under ", _jsx(Span, { weight: "bold", children: "My Account \u2192 My Orders" }), "."] })] })),
29
29
  },
30
30
  {
31
31
  Icon: UserX, title: "Guest Limitations",
@@ -24,7 +24,7 @@ function ToggleRow({ label, description, checked, onChange, disabled, }) {
24
24
  }
25
25
  // ─── Section card ───────────────────────────────────────────────────────────
26
26
  function SectionCard({ title, children }) {
27
- return (_jsxs(Div, { className: `border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] ${__O.hidden}`, rounded: "xl", children: [_jsx(Div, { className: "border-b border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-alt,var(--appkit-color-surface))]", padding: "inline", children: _jsx(Text, { className: "tracking-widest text-[var(--appkit-color-text-muted)]", size: "xs", weight: "semibold", transform: "uppercase", children: title }) }), _jsx(Div, { padding: "x-md", children: children })] }));
27
+ return (_jsxs(Div, { className: `border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] ${__O.hidden}`, rounded: "xl", children: [_jsx(Div, { className: "border-b border-[var(--appkit-color-border)] bg-[var(--appkit-color-bg,var(--appkit-color-surface))]", padding: "inline", children: _jsx(Text, { className: "tracking-widest text-[var(--appkit-color-text-muted)]", size: "xs", weight: "semibold", transform: "uppercase", children: title }) }), _jsx(Div, { padding: "x-md", children: children })] }));
28
28
  }
29
29
  export function NotificationPreferencesPanel({ fetchUrl = ACCOUNT_ENDPOINTS.NOTIFICATION_PREFERENCES, saveUrl = ACCOUNT_ENDPOINTS.NOTIFICATION_PREFERENCES, onSave, }) {
30
30
  const { showToast } = useToast();
@@ -50,6 +50,23 @@ const ORDER_LANE_TABS = [
50
50
  { id: "standard", label: "Normal" },
51
51
  { id: "auction", label: "Auction wins" },
52
52
  { id: "offer", label: "Offer wins" },
53
+ /*
54
+ * 🛑 `"preorder"` WITHOUT A HYPHEN, and this is not a typo to tidy up.
55
+ *
56
+ * `OrderType` (`orders/utils/order-splitter.ts`) spells it `"preorder"`,
57
+ * while `ListingType` spells the same concept `"pre-order"` WITH a hyphen.
58
+ * Two unions, two spellings, and the tab must match the one actually stored
59
+ * on `OrderDocument.orderType` — a tab id matching no stored value returns
60
+ * zero rows forever with no error anywhere (Root Cause #33), which is
61
+ * exactly the failure this file's own comment above warns about.
62
+ *
63
+ * These two lanes existed in the union and had no tab, so pre-order and
64
+ * prize-draw orders were reachable only by landing on "All" and scrolling.
65
+ * Five lanes, four tabs — the eighth instance this session of a
66
+ * hand-written enumeration drifting from the union it filters.
67
+ */
68
+ { id: "preorder", label: "Pre-orders" },
69
+ { id: "prize-draw", label: "Prize draws" },
53
70
  ];
54
71
  /**
55
72
  * Payment-method tabs — a THIRD axis, independent of the lane tabs above and
@@ -97,7 +97,7 @@ export function UserSidebar({ items, groups, mobileOpen = false, onCloseMobile,
97
97
  // ── Persistent sidebar variant (desktop left slide-over, mobile bottom-sheet) ──────
98
98
  if (variant === "sidebar") {
99
99
  const handleToggle = onToggle ?? close;
100
- return (_jsxs(_Fragment, { children: [desktopOpen && (_jsx(Div, { surface: "overlay-xs", className: "hidden lg:block fixed inset-0 backdrop-blur-[2px] z-30", onClick: handleToggle, "aria-hidden": "true" })), _jsxs(Div, { className: `hidden lg:flex fixed ${rail.edgeClass} z-40 transition-transform duration-300 top-[var(--header-height,3.5rem)] h-[calc(100vh-var(--header-height,3.5rem))] w-[13rem] ${desktopOpen ? "translate-x-0" : rail.collapsedTranslateClass}`, children: [_jsxs(Stack, { border: "default", surface: "default", className: `flex-1 ${rail.borderClass} dark:border-[var(--appkit-color-border)] ${__O.hidden}`, shadow: "xl", children: [_jsx(Div, { border: "bottom-subtle", paddingY: "y-sm-tall", className: "shrink-0", padding: "x-md", children: _jsx(Span, { size: "xs", weight: "semibold", transform: "uppercase", color: "muted", children: "My Account" }) }), _jsx(Div, { className: `flex-1 ${__O.yAuto}`, children: _jsx(DrawerContent, { groups: groups, items: items, activeHref: pathname }) }), renderFooter && _jsx(Div, { border: "top", padding: "inline", children: renderFooter() })] }), _jsx(SidebarCollapseToggle, { expanded: desktopOpen, onToggle: handleToggle, edge: hand })] }), _jsx(Div, { className: "lg:hidden", children: _jsx(BottomSheet, { open: mobileOpen, onClose: close, title: "My Account", children: _jsx(DrawerContent, { groups: groups, items: items, activeHref: pathname, onItemClick: close }) }) })] }));
100
+ return (_jsxs(_Fragment, { children: [_jsxs(Div, { className: `hidden lg:flex fixed ${rail.edgeClass} z-40 transition-transform duration-300 top-[var(--header-height,3.5rem)] h-[calc(100vh-var(--header-height,3.5rem))] w-[13rem] ${desktopOpen ? "translate-x-0" : rail.collapsedTranslateClass}`, children: [_jsxs(Stack, { border: "default", surface: "default", className: `flex-1 ${rail.borderClass} dark:border-[var(--appkit-color-border)] ${__O.hidden}`, shadow: "xl", children: [_jsx(Div, { border: "bottom-subtle", paddingY: "y-sm-tall", className: "shrink-0", padding: "x-md", children: _jsx(Span, { size: "xs", weight: "semibold", transform: "uppercase", color: "muted", children: "My Account" }) }), _jsx(Div, { className: `flex-1 ${__O.yAuto}`, children: _jsx(DrawerContent, { groups: groups, items: items, activeHref: pathname }) }), renderFooter && _jsx(Div, { border: "top", padding: "inline", children: renderFooter() })] }), _jsx(SidebarCollapseToggle, { expanded: desktopOpen, onToggle: handleToggle, edge: hand })] }), _jsx(Div, { className: "lg:hidden", children: _jsx(BottomSheet, { open: mobileOpen, onClose: close, title: "My Account", children: _jsx(DrawerContent, { groups: groups, items: items, activeHref: pathname, onItemClick: close }) }) })] }));
101
101
  }
102
102
  // ── Overlay variant (default) — portal on desktop, bottom-sheet on mobile ─
103
103
  const content = (_jsx(DrawerContent, { groups: groups, items: items, activeHref: pathname, onItemClick: close }));
@@ -1,4 +1,5 @@
1
1
  import { useMutation, useQuery } from "@tanstack/react-query";
2
+ import { useOptionalSession } from "../../../react/contexts/SessionContext.js";
2
3
  import { apiClient } from "../../../http/index.js";
3
4
  import { NOTIFICATIONS_ENDPOINTS } from "../../../constants/api-endpoints.js";
4
5
  /**
@@ -10,7 +11,35 @@ export function useNotifications(limit = 10, options) {
10
11
  const notificationsEndpoint = options?.notificationsEndpoint ?? NOTIFICATIONS_ENDPOINTS.LIST;
11
12
  const readAllEndpoint = options?.readAllEndpoint ?? NOTIFICATIONS_ENDPOINTS.READ_ALL;
12
13
  const readEndpoint = options?.readEndpoint ?? NOTIFICATIONS_ENDPOINTS.BY_ID;
14
+ /*
15
+ * 🛑 SIGNED-OUT VISITORS WERE POLLING AN AUTH-ONLY ENDPOINT.
16
+ *
17
+ * The interval below was tuned carefully and at length — and the query had
18
+ * no `enabled` gate at all. `TitleBar` mounts this hook on every page, so a
19
+ * signed-out visitor fetched `/api/notifications` on mount and again every
20
+ * five minutes per open tab, collecting a 401 each time. That is the
21
+ * audit's "two 401s fire for signed-out visitors, including a notifications
22
+ * poll that should not run at all", and signed-out is the majority of
23
+ * traffic on a public catalogue.
24
+ *
25
+ * Every one of those was a billed function invocation (Rule #6) spent
26
+ * asking for the unread count of nobody.
27
+ *
28
+ * `useOptionalSession`, not `useSession`: this hook is library code mounted
29
+ * in shared chrome, so it must not require a provider — and with no
30
+ * provider it fails CLOSED, which is the safe direction for a request that
31
+ * can only 401. Same precedent as `useCanSeePrices`.
32
+ *
33
+ * `loading` is deliberately part of the gate. `SessionProvider` mounts
34
+ * `initialUser={null}` (the root layout must stay static), so `user` is
35
+ * null on every hard load until Firebase resolves — firing then would 401
36
+ * for a signed-IN user too, and firing again afterwards would double the
37
+ * request.
38
+ */
39
+ const session = useOptionalSession();
40
+ const isSignedIn = Boolean(session?.user?.uid) && !session?.loading;
13
41
  const { data, isLoading, refetch } = useQuery({
42
+ enabled: isSignedIn,
14
43
  queryKey: ["notifications", "list", String(limit)],
15
44
  queryFn: () => apiClient.get(`${notificationsEndpoint}?limit=${limit}`),
16
45
  staleTime: NOTIFICATIONS_POLL_MS,
@@ -7,6 +7,8 @@
7
7
  import { couponsRepository } from "../../promotions/repository/coupons.repository.js";
8
8
  import { NotFoundError, ValidationError } from "../../../errors/index.js";
9
9
  import { serverLogger } from "../../../monitoring/index.js";
10
+ import { normalizeError } from "../../../errors/normalize.js";
11
+ import { claimedCouponsRepository } from "../../promotions/repository/claimed-coupons.repository.js";
10
12
  import { recordAdminAction } from "../../../_internal/server/features/audit-log/actions.js";
11
13
  import { AdminAuditActionValues } from "../../audit-log/schemas/firestore.js";
12
14
  export async function adminCreateCoupon(adminId, input) {
@@ -58,6 +60,40 @@ export async function adminUpdateCoupon(adminId, id, input) {
58
60
  : {}),
59
61
  };
60
62
  const updated = await couponsRepository.update(id, updateData);
63
+ /*
64
+ * D59 — re-sync every wallet's denormalised snapshot. Nothing did this, so
65
+ * `/user/coupons` kept rendering the terms as they were at CLAIM time while
66
+ * checkout applied the live ones.
67
+ *
68
+ * 🛑 Display only, verified: redemption reads the live coupon via
69
+ * `getCouponByCode`, so the drift never changed what was charged. Fixing it
70
+ * stops the wallet advertising terms checkout will not honour.
71
+ *
72
+ * Best-effort and fire-and-forget: the admin's edit has already committed,
73
+ * and a failed re-sync must not fail it. The fallback is a stale display —
74
+ * which is exactly today's behaviour, so the worst case is no regression.
75
+ */
76
+ void claimedCouponsRepository
77
+ .resyncSnapshotsForCoupon(id, {
78
+ name: updated.name,
79
+ ...(updated.description ? { description: updated.description } : {}),
80
+ type: updated.type,
81
+ scope: updated.scope ?? "admin",
82
+ ...(updated.storeId ? { storeId: updated.storeId } : {}),
83
+ discount: updated.discount,
84
+ restrictions: updated.restrictions,
85
+ })
86
+ .then((n) => {
87
+ if (n > 0)
88
+ serverLogger.info("adminUpdateCoupon: wallet snapshots re-synced", { couponId: id, wallets: n });
89
+ if (n === 500) {
90
+ serverLogger.warn("adminUpdateCoupon: snapshot re-sync hit its 500-wallet bound — later wallets still show stale terms", { couponId: id });
91
+ }
92
+ })
93
+ .catch((err) => {
94
+ void normalizeError(err);
95
+ serverLogger.warn("adminUpdateCoupon: wallet snapshot re-sync failed (display only)", { couponId: id });
96
+ });
61
97
  serverLogger.info("adminUpdateCoupon", {
62
98
  adminId,
63
99
  couponId: id,
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /**
4
4
  * AdminBeysView — browse and correct the generated Beyblade database.
5
5
  *
@@ -174,5 +174,5 @@ export function AdminBeysView({ children, ...props }) {
174
174
  if (React.Children.count(children) > 0) {
175
175
  return (_jsxs(ListingLayout, { portal: "admin", ...props, children: [_jsx(Heading, { level: 1, className: "sr-only", children: "Beyblades" }), children] }));
176
176
  }
177
- return (_jsxs(_Fragment, { children: [_jsx(Heading, { level: 1, className: "sr-only", children: "Beyblades" }), _jsx(DataListingView, { config: ADMIN_BEYS_CONFIG })] }));
177
+ return _jsx(DataListingView, { config: ADMIN_BEYS_CONFIG });
178
178
  }
@@ -207,7 +207,7 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
207
207
  }, placeholder: "e.g. How to Grade Pok\u00E9mon Cards" }), _jsx(Input, { label: "Slug", value: values.slug, onChange: (e) => {
208
208
  setSlugManual(true);
209
209
  onChange({ slug: e.target.value });
210
- }, placeholder: "blog-how-to-grade-pokemon-cards", helperText: "Auto-generated from title. Must start with 'blog-'." }), _jsx(Input, { label: "Excerpt", value: values.excerpt, onChange: (e) => onChange({ excerpt: e.target.value }), placeholder: "Short summary shown in listings and cards" }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Content" }), _jsx(RichTextEditor, { value: values.content, onChange: (v) => onChange({ content: v }), placeholder: "Write your article here...", minHeightClassName: "min-h-[320px]" })] })] })),
210
+ }, placeholder: "blog-how-to-grade-pokemon-cards", helperText: "Auto-generated from title. Must start with 'blog-'." }), _jsx(Input, { label: "Excerpt", value: values.excerpt, onChange: (e) => onChange({ excerpt: e.target.value }), placeholder: "Short summary shown in listings and cards" }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Content" }), _jsx(RichTextEditor, { ariaLabel: "Content", value: values.content, onChange: (v) => onChange({ content: v }), placeholder: "Write your article here...", minHeightClassName: "min-h-[320px]" })] })] })),
211
211
  },
212
212
  {
213
213
  id: "media",
@@ -265,7 +265,7 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
265
265
  if (embedded) {
266
266
  return _jsx(Div, { className: `${__O.yAuto} ${__P.p4}`, children: formContent });
267
267
  }
268
- const previewSection = (_jsxs(Div, { rounded: "lg", padding: "5", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-raised)] max-h-[calc(100vh-12rem)] overflow-y-auto", children: [_jsx(Row, { textSize: "xs", textWeight: "semibold", color: "muted", className: "mb-3 uppercase tracking-wide", align: "center", gap: "sm", children: _jsx(Span, { children: "Live preview" }) }), draft.coverImage ? (_jsx(Div, { className: "relative h-64 w-full mb-4", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: draft.coverImage, alt: "", size: "hero" }) })) : null, _jsx(Heading, { level: 1, className: "mb-1", size: "2xl", weight: "bold", children: draft.title || "Untitled post" }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)] mb-4", size: "xs", children: [draft.authorName || "Anonymous", draft.category ? ` · ${draft.category}` : ""] }), draft.excerpt ? (_jsx(Text, { className: "italic text-[var(--appkit-color-text-secondary)] mb-4", children: draft.excerpt })) : null, _jsx(RichTextRenderer, { html: draft.content || "<em>No content yet…</em>" }), draft.tags.length > 0 ? (_jsx(Row, { wrap: true, gap: "xs", className: "mt-6", children: draft.tags.map((t) => (_jsxs(Span, { size: "xs", className: "bg-[var(--appkit-color-surface)] border border-[var(--appkit-color-border)]", rounded: "full", padding: "pill-xs", children: ["#", t] }, t))) })) : null] }, "preview"));
268
+ const previewSection = (_jsxs(Div, { rounded: "lg", padding: "5", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-raised)] max-h-[calc(100vh-12rem)] overflow-y-auto", children: [_jsx(Row, { textSize: "xs", textWeight: "semibold", color: "muted", className: "mb-3 uppercase tracking-wide", align: "center", gap: "sm", children: _jsx(Span, { children: "Live preview" }) }), draft.coverImage ? (_jsx(Div, { className: "relative h-64 w-full mb-4", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: draft.coverImage, alt: "", size: "hero" }) })) : null, _jsx(Heading, { level: 1, className: "mb-1", size: "2xl", weight: "bold", children: draft.title || "Untitled post" }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)] mb-4", size: "xs", children: [draft.authorName || "Anonymous", draft.category ? ` · ${draft.category}` : ""] }), draft.excerpt ? (_jsx(Text, { className: "italic text-[var(--appkit-color-text-muted)] mb-4", children: draft.excerpt })) : null, _jsx(RichTextRenderer, { html: draft.content || "<em>No content yet…</em>" }), draft.tags.length > 0 ? (_jsx(Row, { wrap: true, gap: "xs", className: "mt-6", children: draft.tags.map((t) => (_jsxs(Span, { size: "xs", className: "bg-[var(--appkit-color-surface)] border border-[var(--appkit-color-border)]", rounded: "full", padding: "pill-xs", children: ["#", t] }, t))) })) : null] }, "preview"));
269
269
  return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Post" : "New Blog Post", sections: [
270
270
  _jsxs(Div, { className: "lg:grid lg:grid-cols-[3fr_2fr] lg:gap-[1.5rem]", children: [_jsx(Div, { children: formContent }), _jsx(Div, { className: "mt-6 lg:mt-0 lg:sticky lg:top-4 lg:self-start", children: previewSection })] }, "split"),
271
271
  ] }));
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /**
4
4
  * Create or edit a brand.
5
5
  *
@@ -20,7 +20,8 @@ import { useQuery } from "@tanstack/react-query";
20
20
  import { useApiMutation } from "@mohasinac/appkit/client";
21
21
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
22
22
  import { apiClient } from "../../../http/index.js";
23
- import { ConfirmDeleteModal, Div, StackedViewShell, useToast } from "../../../ui/index.js";
23
+ import { ConfirmDeleteModal, Div, Input, StackedViewShell, Textarea, useToast } from "../../../ui/index.js";
24
+ import { Repeater } from "../../../ui/components/Repeater.js";
24
25
  import { applyZodIssues } from "../../../ui/forms/apply-zod-issues.js";
25
26
  import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary.js";
26
27
  import { FormShellContext, useFormShellState } from "../../../ui/forms/FormShell.js";
@@ -41,6 +42,8 @@ const EMPTY = {
41
42
  founded: "",
42
43
  isActive: true,
43
44
  displayOrder: "",
45
+ highlightsText: "",
46
+ faqs: [],
44
47
  };
45
48
  function toBrandSlug(str) {
46
49
  const base = str
@@ -82,6 +85,8 @@ export function AdminBrandEditorView({ brandId, onSaved, onDeleted, embedded, ..
82
85
  founded: brand.founded !== undefined ? String(brand.founded) : "",
83
86
  isActive: brand.isActive ?? true,
84
87
  displayOrder: brand.displayOrder !== undefined ? String(brand.displayOrder) : "",
88
+ highlightsText: (brand.highlights ?? []).join("\n"),
89
+ faqs: brand.faqs ?? [],
85
90
  });
86
91
  }, [brandQuery.data]);
87
92
  const sections = React.useMemo(() => buildSectionsFromSchema(brandFormSchema, {
@@ -90,6 +95,16 @@ export function AdminBrandEditorView({ brandId, onSaved, onDeleted, embedded, ..
90
95
  type: "brand-logo",
91
96
  brand: values.name || values.slug,
92
97
  }), onChange: (url) => onChange({ logoURL: url }) })),
98
+ /*
99
+ * The FAQ rows. `Repeater` is the one promoted out of
100
+ * `AdminCategoryEditorView` on the condition its own author set —
101
+ * "promote it when a fourth caller appears outside this file" — so
102
+ * this is that caller rather than a second implementation.
103
+ */
104
+ faqs: ({ values, onChange }) => {
105
+ const rows = values.faqs ?? [];
106
+ return (_jsx(Repeater, { rows: rows, onChange: (next) => onChange({ faqs: next }), blank: () => ({ question: "", answer: "" }), addLabel: "Add a question", max: 8, renderRow: (row, set) => (_jsxs(_Fragment, { children: [_jsx(Input, { label: "Question", value: row.question, onChange: (e) => set({ question: e.target.value }), placeholder: "Are Takara-Tomy releases region-locked?" }), _jsx(Textarea, { label: "Answer", rows: 3, value: row.answer, onChange: (e) => set({ answer: e.target.value }) })] })) }));
107
+ },
93
108
  },
94
109
  }), [upload]);
95
110
  const nav = useSectionFormNav(sections, form, { scope: "admin:brand-editor" });
@@ -112,6 +127,21 @@ export function AdminBrandEditorView({ brandId, onSaved, onDeleted, embedded, ..
112
127
  founded: optionalNumber(form.founded),
113
128
  isActive: form.isActive,
114
129
  displayOrder: optionalNumber(form.displayOrder),
130
+ /*
131
+ * 🛑 Sent even when EMPTY, which is what makes "remove the last
132
+ * highlight" work. The mapper tests `!== undefined`, so an empty
133
+ * array clears the list; omitting the key instead would leave the
134
+ * stored value untouched and the admin unable to delete what they
135
+ * can see.
136
+ *
137
+ * Blank lines and whitespace-only rows are dropped rather than
138
+ * stored — an empty bullet renders as a gap on the brand page.
139
+ */
140
+ highlights: form.highlightsText
141
+ .split("\n")
142
+ .map((l) => l.trim())
143
+ .filter((l) => l.length > 0),
144
+ faqs: form.faqs.filter((f) => f.question.trim() && f.answer.trim()),
115
145
  };
116
146
  if (isEdit)
117
147
  return apiClient.put(ADMIN_ENDPOINTS.BRAND_BY_ID(brandId), payload);
@@ -134,7 +134,7 @@ export function AdminBundleEditorView({ bundleId, onSaved, onDeleted, scope = "a
134
134
  * would have written that blank over a live bundle.
135
135
  *
136
136
  * Verified in production 2026-10-02: /admin/bundles/
137
- * bundle-every-generation-starter-pack/edit polled for 14s with
137
+ * bundle-x-bx53-complete/edit polled for 14s with
138
138
  * `input[name="name"]` and `input[name="priceRupees"]` both `""` and no
139
139
  * member count, while `GET /api/admin/bundles/{id}` returned 200 with the
140
140
  * real bundle (`{ success, data: <bundle> }`, no nested `data`). I did not
@@ -76,7 +76,7 @@ function ZonePicker({ selected, onChange, disabled, }) {
76
76
  // ── Background Editor ────────────────────────────────────────────────────────
77
77
  function BackgroundEditor({ value, onChange, }) {
78
78
  const set = (patch) => onChange({ ...value, ...patch });
79
- return (_jsxs(Stack, { gap: "3", children: [_jsx(Select, { label: "Background type", value: value.type, onChange: (e) => onChange({ type: e.target.value }), options: BG_TYPE_OPTIONS }), (value.type === "image" || value.type === "video") && (_jsxs(_Fragment, { children: [_jsx(Input, { label: `${value.type === "image" ? "Image" : "Video"} URL`, value: value.url ?? "", onChange: (e) => set({ url: e.target.value }), placeholder: "https://..." }), _jsx(Input, { label: "Mobile URL (optional)", value: value.mobileUrl ?? "", onChange: (e) => set({ mobileUrl: e.target.value || undefined }), placeholder: "https://... (portrait crop)" }), value.type === "video" && (_jsx(Input, { label: "Poster / thumbnail URL", value: value.thumbnail ?? "", onChange: (e) => set({ thumbnail: e.target.value || undefined }), placeholder: "https://..." })), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", rounded: "lg", border: "default", children: [_jsx(Toggle, { label: "Dim overlay", checked: value.dimOverlay?.enabled ?? false, onChange: (v) => set({ dimOverlay: { enabled: v, opacity: value.dimOverlay?.opacity ?? 40 } }) }), value.dimOverlay?.enabled && (_jsxs(Div, { children: [_jsxs(Text, { className: "mb-1", size: "sm", weight: "medium", children: ["Opacity (", value.dimOverlay.opacity, "%)"] }), _jsx(Slider, { min: 0, max: 90, step: 5, value: value.dimOverlay.opacity, onChange: (opacity) => set({ dimOverlay: { enabled: true, opacity } }), showValue: false })] }))] })] })), value.type === "color" && (_jsx(Input, { label: "Colour (CSS token or hex)", value: value.color ?? "", onChange: (e) => set({ color: e.target.value }), placeholder: "var(--appkit-color-primary) or #1a1a2e" })), value.type === "gradient" && (_jsxs(_Fragment, { children: [_jsx(Input, { label: "From colour", value: value.gradientFrom ?? "", onChange: (e) => set({ gradientFrom: e.target.value }), placeholder: "var(--appkit-color-primary)" }), _jsx(Input, { label: "To colour", value: value.gradientTo ?? "", onChange: (e) => set({ gradientTo: e.target.value }), placeholder: "var(--appkit-color-secondary)" }), _jsx(Input, { label: "Angle (degrees)", type: "number", value: String(value.gradientAngle ?? 135), onChange: (e) => set({ gradientAngle: Number(e.target.value) }), min: 0, max: 360 })] }))] }));
79
+ return (_jsxs(Stack, { gap: "3", children: [_jsx(Select, { label: "Background type", value: value.type, onChange: (e) => onChange({ type: e.target.value }), options: BG_TYPE_OPTIONS }), (value.type === "image" || value.type === "video") && (_jsxs(_Fragment, { children: [_jsx(Input, { label: `${value.type === "image" ? "Image" : "Video"} URL`, value: value.url ?? "", onChange: (e) => set({ url: e.target.value }), placeholder: "https://..." }), _jsx(Input, { label: "Mobile URL (optional)", value: value.mobileUrl ?? "", onChange: (e) => set({ mobileUrl: e.target.value || undefined }), placeholder: "https://... (portrait crop)" }), value.type === "video" && (_jsx(Input, { label: "Poster / thumbnail URL", value: value.thumbnail ?? "", onChange: (e) => set({ thumbnail: e.target.value || undefined }), placeholder: "https://..." })), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", rounded: "lg", border: "default", children: [_jsx(Toggle, { label: "Dim overlay", checked: value.dimOverlay?.enabled ?? false, onChange: (v) => set({ dimOverlay: { enabled: v, opacity: value.dimOverlay?.opacity ?? 40 } }) }), value.dimOverlay?.enabled && (_jsxs(Div, { children: [_jsxs(Text, { className: "mb-1", size: "sm", weight: "medium", children: ["Opacity (", value.dimOverlay.opacity, "%)"] }), _jsx(Slider, { "aria-label": `Dim overlay opacity, ${value.dimOverlay.opacity}%`, min: 0, max: 90, step: 5, value: value.dimOverlay.opacity, onChange: (opacity) => set({ dimOverlay: { enabled: true, opacity } }), showValue: false })] }))] })] })), value.type === "color" && (_jsx(Input, { label: "Colour (CSS token or hex)", value: value.color ?? "", onChange: (e) => set({ color: e.target.value }), placeholder: "var(--appkit-color-primary) or #1a1a2e" })), value.type === "gradient" && (_jsxs(_Fragment, { children: [_jsx(Input, { label: "From colour", value: value.gradientFrom ?? "", onChange: (e) => set({ gradientFrom: e.target.value }), placeholder: "var(--appkit-color-primary)" }), _jsx(Input, { label: "To colour", value: value.gradientTo ?? "", onChange: (e) => set({ gradientTo: e.target.value }), placeholder: "var(--appkit-color-secondary)" }), _jsx(Input, { label: "Angle (degrees)", type: "number", value: String(value.gradientAngle ?? 135), onChange: (e) => set({ gradientAngle: Number(e.target.value) }), min: 0, max: 360 })] }))] }));
80
80
  }
81
81
  // ── Card Editor ──────────────────────────────────────────────────────────────
82
82
  function CardEditor({ card, index, otherZones, onChange, onRemove, }) {
@@ -152,5 +152,5 @@ export function AdminCategoriesView({ children, ...props }) {
152
152
  if (React.Children.count(children) > 0) {
153
153
  return (_jsxs(ListingLayout, { portal: "admin", ...props, children: [_jsx(Heading, { level: 1, className: "sr-only", children: "Categories" }), children] }));
154
154
  }
155
- return (_jsxs(_Fragment, { children: [_jsx(Heading, { level: 1, className: "sr-only", children: "Categories" }), _jsx(DataListingView, { config: ADMIN_CATEGORIES_CONFIG })] }));
155
+ return _jsx(DataListingView, { config: ADMIN_CATEGORIES_CONFIG });
156
156
  }
@@ -4,11 +4,14 @@ import { useApiMutation } from "@mohasinac/appkit/client";
4
4
  import React from "react";
5
5
  import { useQuery } from "@tanstack/react-query";
6
6
  import { z } from "zod";
7
- import { Button, Card, CardBody, ConfirmDeleteModal, Div, Input, PaginatedSelect, Select, Span, Stack, StackedViewShell, Text, Textarea, Toggle, useToast, Show, StickyToolbar } from "../../../ui/index.js";
7
+ import { Card, CardBody, ConfirmDeleteModal, Div, Input, PaginatedSelect, Select, Span, Stack, StackedViewShell, Text, Textarea, Toggle, useToast, Show, StickyToolbar } from "../../../ui/index.js";
8
8
  import { usedPlaceholders } from "../../../_internal/shared/templating/placeholders.js";
9
9
  import { apiClient } from "../../../http/index.js";
10
10
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
11
11
  import { CategoryQuickCreateForm } from "./CategoryQuickCreateForm.js";
12
+ import { useMediaUpload } from "../../media/index.js";
13
+ import { ImageUpload } from "../../media/upload/ImageUpload.js";
14
+ import { Repeater } from "../../../ui/components/Repeater.js";
12
15
  import { FieldInput, FormErrorSummary, FormShellContext, useFormShellState } from "../../../ui/forms/index.js";
13
16
  import { SectionForm, useSectionFormNav } from "../../shell/index.js";
14
17
  /*
@@ -59,17 +62,6 @@ const CONDITION_DEFAULT_OPTIONS = [
59
62
  { value: "used", label: "Used" },
60
63
  { value: "refurbished", label: "Refurbished" },
61
64
  ];
62
- /**
63
- * A minimal add/remove repeater.
64
- *
65
- * Deliberately NOT a new appkit primitive: three local shapes with three
66
- * different row renderers is not yet the Rule of Three on a *component* — it
67
- * is one container used three times with a render prop, which is what this is.
68
- * Promote it when a fourth caller appears outside this file.
69
- */
70
- function Repeater({ rows, onChange, blank, addLabel, renderRow, max, }) {
71
- return (_jsxs(Stack, { gap: "sm", children: [rows.map((row, i) => (_jsx(Div, { rounded: "lg", border: "default", padding: "sm", children: _jsxs(Stack, { gap: "xs", children: [renderRow(row, (patch) => onChange(rows.map((r, j) => (j === i ? { ...r, ...patch } : r)))), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => onChange(rows.filter((_, j) => j !== i)), children: "Remove" })] }) }, i))), rows.length < max ? (_jsx(Button, { variant: "secondary", size: "sm", onClick: () => onChange([...rows, blank()]), children: addLabel })) : (_jsxs(Text, { size: "xs", color: "muted", children: ["Maximum of ", max, " reached."] }))] }));
72
- }
73
65
  async function loadTaxCodeOptions(query, page) {
74
66
  const params = new URLSearchParams({ page: String(page), pageSize: "25" });
75
67
  if (query)
@@ -108,6 +100,36 @@ const categoryFormSchema = z.object({
108
100
  .or(z.literal("")),
109
101
  isActive: z.boolean().optional(),
110
102
  showInMenu: z.boolean().optional(),
103
+ /*
104
+ * ── Discovery flags ──────────────────────────────────────────────────────
105
+ *
106
+ * All four are in `CATEGORIES_UPDATABLE_FIELDS`, so the repository has
107
+ * always accepted them — there was simply no form. An admin could not
108
+ * feature a category, set its priority, or take it out of search, on a
109
+ * taxonomy of 1,185 rows.
110
+ */
111
+ isFeatured: z.boolean().optional(),
112
+ featuredPriority: z
113
+ .string()
114
+ .regex(/^\d*$/, "Priority must be a whole number")
115
+ .optional()
116
+ .or(z.literal("")),
117
+ isSearchable: z.boolean().optional(),
118
+ showOnHomepage: z.boolean().optional(),
119
+ /*
120
+ * `display.coverImage`, flattened into the draft because the form is flat.
121
+ *
122
+ * 🛑 THE DATA MODEL HAS ALWAYS CARRIED THIS AND THE EDITOR NEVER EXPOSED IT.
123
+ * 504 category covers live in Storage, `resolveTaxonomyVisual` reads
124
+ * `display.coverImage`, and the category page renders it — the only missing
125
+ * piece was a way for an admin to set or change one. That is why the
126
+ * taxonomy looked unfinished.
127
+ *
128
+ * Not validated as a URL: the value is either a `/media/<slug>` path minted
129
+ * by the upload flow or empty, and a stricter rule here would reject the
130
+ * former. An empty string is a legitimate value and means "cleared".
131
+ */
132
+ coverImage: z.string().optional().or(z.literal("")),
111
133
  // ── Content & Templates ──────────────────────────────────────────────────
112
134
  contentBody: z
113
135
  .string()
@@ -151,6 +173,13 @@ const EMPTY_VALUES = {
151
173
  order: "",
152
174
  isActive: true,
153
175
  showInMenu: true,
176
+ isFeatured: false,
177
+ featuredPriority: "",
178
+ /* Searchable by DEFAULT — a new category nobody can find is a worse
179
+ default than one that appears in results. */
180
+ isSearchable: true,
181
+ showOnHomepage: false,
182
+ coverImage: "",
154
183
  contentBody: "",
155
184
  highlightsText: "",
156
185
  hasbroName: "",
@@ -277,6 +306,7 @@ function toCategorySlug(str) {
277
306
  export function AdminCategoryEditorView({ categoryId, onSaved, onDeleted, embedded, ...rest }) {
278
307
  const isEdit = Boolean(categoryId);
279
308
  const [deleteOpen, setDeleteOpen] = React.useState(false);
309
+ const { upload } = useMediaUpload();
280
310
  const [values, setValues] = React.useState(EMPTY_VALUES);
281
311
  /*
282
312
  * The three repeater arrays live OUTSIDE `values`.
@@ -330,6 +360,11 @@ export function AdminCategoryEditorView({ categoryId, onSaved, onDeleted, embedd
330
360
  order: cat.order !== undefined ? String(cat.order) : "",
331
361
  isActive: cat.isActive ?? true,
332
362
  showInMenu: cat.display?.showInMenu ?? true,
363
+ isFeatured: cat.isFeatured ?? false,
364
+ featuredPriority: cat.featuredPriority !== undefined ? String(cat.featuredPriority) : "",
365
+ isSearchable: cat.isSearchable ?? true,
366
+ showOnHomepage: cat.showOnHomepage ?? false,
367
+ coverImage: cat.display?.coverImage ?? "",
333
368
  contentBody: cat.contentBody ?? "",
334
369
  highlightsText: listToLines(cat.highlights),
335
370
  hasbroName: cat.hasbroName ?? "",
@@ -366,7 +401,22 @@ export function AdminCategoryEditorView({ categoryId, onSaved, onDeleted, embedd
366
401
  parentId: String(values.parentId ?? "") || undefined,
367
402
  order: values.order !== "" && values.order !== undefined ? Number(values.order) : undefined,
368
403
  isActive: values.isActive ?? true,
369
- display: { showInMenu: values.showInMenu ?? true },
404
+ isFeatured: values.isFeatured ?? false,
405
+ featuredPriority: values.featuredPriority !== "" && values.featuredPriority !== undefined
406
+ ? Number(values.featuredPriority)
407
+ : undefined,
408
+ isSearchable: values.isSearchable ?? true,
409
+ showOnHomepage: values.showOnHomepage ?? false,
410
+ /*
411
+ * `coverImage` is sent even when empty — an empty string is how the
412
+ * admin says "remove the cover", and omitting it would make clearing
413
+ * the field a no-op now that the route merges rather than replaces.
414
+ * Every reader trims and tests truthiness, so `""` reads as absent.
415
+ */
416
+ display: {
417
+ showInMenu: values.showInMenu ?? true,
418
+ coverImage: String(values.coverImage ?? ""),
419
+ },
370
420
  };
371
421
  if (isEdit) {
372
422
  /*
@@ -434,8 +484,14 @@ export function AdminCategoryEditorView({ categoryId, onSaved, onDeleted, embedd
434
484
  {
435
485
  id: "visibility",
436
486
  label: "Display",
437
- fields: ["order", "isActive", "showInMenu"],
438
- render: ({ values: v, onChange, errors }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Input, { label: "Display order", value: String(v.order ?? ""), onChange: (e) => onChange({ order: e.target.value }), type: "number", min: 0, placeholder: "0", error: errors.order }), _jsx(Toggle, { label: "Active", checked: v.isActive ?? true, onChange: (checked) => onChange({ isActive: checked }) }), _jsx(Toggle, { label: "Show in menu", checked: v.showInMenu ?? true, onChange: (checked) => onChange({ showInMenu: checked }) })] })),
487
+ fields: ["order", "isActive", "showInMenu", "coverImage", "isFeatured", "featuredPriority", "isSearchable", "showOnHomepage"],
488
+ render: ({ values: v, onChange, errors }) => (_jsxs(Stack, { gap: "md", children: [_jsx(ImageUpload, { label: "Cover image", helperText: "Shown on the category page hero and on category cards.", currentImage: String(v.coverImage ?? ""), onUpload: (file) => upload(file, "categories", true, {
489
+ type: "category-image",
490
+ // The filename is content-derived, so re-uploading for the
491
+ // same category overwrites at the same storage path rather
492
+ // than orphaning the previous object.
493
+ name: String(v.name ?? "") || String(v.slug ?? ""),
494
+ }), onChange: (url) => onChange({ coverImage: url }) }), _jsx(Input, { label: "Display order", value: String(v.order ?? ""), onChange: (e) => onChange({ order: e.target.value }), type: "number", min: 0, placeholder: "0", error: errors.order }), _jsx(Toggle, { label: "Active", checked: v.isActive ?? true, onChange: (checked) => onChange({ isActive: checked }) }), _jsx(Toggle, { label: "Show in menu", checked: v.showInMenu ?? true, onChange: (checked) => onChange({ showInMenu: checked }) }), _jsx(Toggle, { label: "Searchable", checked: v.isSearchable ?? true, onChange: (checked) => onChange({ isSearchable: checked }) }), _jsx(Toggle, { label: "Featured", checked: v.isFeatured ?? false, onChange: (checked) => onChange({ isFeatured: checked }) }), (v.isFeatured ?? false) ? (_jsx(Input, { label: "Featured priority", value: String(v.featuredPriority ?? ""), onChange: (e) => onChange({ featuredPriority: e.target.value }), type: "number", min: 0, placeholder: "0", helperText: "Lower sorts first among featured categories.", error: errors.featuredPriority })) : null, _jsx(Toggle, { label: "Show on homepage", checked: v.showOnHomepage ?? false, onChange: (checked) => onChange({ showOnHomepage: checked }) })] })),
439
495
  },
440
496
  /*
441
497
  * 🛑 EDIT ONLY. The create route's `z.object()` would strip every one of
@@ -448,7 +504,7 @@ export function AdminCategoryEditorView({ categoryId, onSaved, onDeleted, embedd
448
504
  id: "content",
449
505
  label: "Content & Templates",
450
506
  fields: ["contentBody", "highlightsText", "hasbroName"],
451
- render: ({ values: v, onChange, errors }) => (_jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", children: "Category page body" }), _jsx(Textarea, { value: String(v.contentBody ?? ""), onChange: (e) => onChange({ contentBody: e.target.value }), rows: 8, placeholder: "400\u2013900 characters of keyword-rich HTML with links to sibling and parent categories\u2026" }), errors.contentBody ? (_jsx(Text, { size: "sm", variant: "error", role: "alert", children: errors.contentBody })) : (_jsx(Text, { size: "xs", color: "muted", children: "Rendered on the category page. Distinct from the short Description above, which also feeds the SEO meta and every card. 36 of worldhobbyshop's 46 categories carry one of these and it is their actual ranking mechanism; no Shopify competitor in the crawl has any." }))] }), _jsx(FieldInput, { name: "hasbroName", label: "Hasbro / international name", value: String(v.hasbroName ?? ""), onChange: (val) => onChange({ hasbroName: val }), placeholder: "e.g. Beyblade Burst Turbo", error: errors.hasbroName, hint: "Only when the Hasbro season name genuinely differs from the Takara-Tomy one \u2014 Cho-Z ships as Turbo, God as Evolution. Rendered on the category page as 'Also known as'. Leave blank if there is no divergence, or if you cannot confirm one: a guessed pairing reads as fact." }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", children: "Highlights \u2014 one per line" }), _jsx(Textarea, { value: String(v.highlightsText ?? ""), onChange: (e) => onChange({ highlightsText: e.target.value }), rows: 4, placeholder: "Genuine Takara Tomy stock\nChecked and photographed before listing" }), _jsx(Text, { size: "xs", color: "muted", children: "Short \"why shop this\" bullets, shown above the FAQs. Blank lines are dropped. Up to 12." })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", children: "Category FAQs" }), _jsx(Text, { size: "xs", color: "muted", children: "Rendered as an accordion on the category page and emitted as FAQPage structured data. Keep platform-wide answers in the global FAQ collection instead \u2014 two copies of one answer means the one that gets fixed is never the one being read." }), _jsx(Repeater, { rows: faqs, onChange: setFaqs, blank: () => ({ question: "", answer: "" }), addLabel: "Add an FAQ", max: 20, renderRow: (row, set) => (_jsxs(_Fragment, { children: [_jsx(Input, { label: "Question", value: row.question, onChange: (e) => set({ question: e.target.value }), placeholder: "Are these compatible with Burst launchers?" }), _jsx(Textarea, { value: row.answer, onChange: (e) => set({ answer: e.target.value }), rows: 3, placeholder: "Answer\u2026" })] })) })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", children: "Description templates" }), _jsxs(Text, { size: "xs", color: "muted", children: ["What the seller form offers when a listing is filed here. One per condition band \u2014 worldhobbyshop reuses exactly two across 1,195 listings. Slots like", " ", _jsx(Span, { family: "mono", children: "{{title}}" }), ",", " ", _jsx(Span, { family: "mono", children: "{{brand}}" }), ",", " ", _jsx(Span, { family: "mono", children: "{{condition}}" }), " and", " ", _jsx(Span, { family: "mono", children: "{{series}}" }), " are filled from the listing; an unresolved one becomes empty and is reported to the seller, never left as a literal brace pair."] }), _jsx(Repeater, { rows: templates, onChange: setTemplates, blank: () => ({ id: "", label: "", variant: "default", body: "" }), addLabel: "Add a template", max: 8, renderRow: (row, set) => (_jsxs(_Fragment, { children: [_jsxs(Div, { layout: "grid", gap: "4", className: "sm:grid-cols-2", children: [_jsx(Input, { label: "Label", value: row.label, onChange: (e) => set({ label: e.target.value }), placeholder: "Pre-owned \u2014 condition as pictured" }), _jsx(Select, { label: "Condition band", value: row.variant, onChange: (e) => set({ variant: e.target.value }), options: TEMPLATE_VARIANT_OPTIONS })] }), _jsx(Textarea, { value: row.body, onChange: (e) => set({ body: e.target.value }), rows: 6, placeholder: "Important things to know about {{title}} \u2014 HTML permitted\u2026" }), _jsx(Text, { size: "xs", color: "muted", children: usedPlaceholders(row.body).length > 0
507
+ render: ({ values: v, onChange, errors }) => (_jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Textarea, { label: "Category page body", value: String(v.contentBody ?? ""), onChange: (e) => onChange({ contentBody: e.target.value }), rows: 8, placeholder: "400\u2013900 characters of keyword-rich HTML with links to sibling and parent categories\u2026" }), errors.contentBody ? (_jsx(Text, { size: "sm", variant: "error", role: "alert", children: errors.contentBody })) : (_jsx(Text, { size: "xs", color: "muted", children: "Rendered on the category page. Distinct from the short Description above, which also feeds the SEO meta and every card. 36 of worldhobbyshop's 46 categories carry one of these and it is their actual ranking mechanism; no Shopify competitor in the crawl has any." }))] }), _jsx(FieldInput, { name: "hasbroName", label: "Hasbro / international name", value: String(v.hasbroName ?? ""), onChange: (val) => onChange({ hasbroName: val }), placeholder: "e.g. Beyblade Burst Turbo", error: errors.hasbroName, hint: "Only when the Hasbro season name genuinely differs from the Takara-Tomy one \u2014 Cho-Z ships as Turbo, God as Evolution. Rendered on the category page as 'Also known as'. Leave blank if there is no divergence, or if you cannot confirm one: a guessed pairing reads as fact." }), _jsxs(Stack, { gap: "xs", children: [_jsx(Textarea, { label: "Highlights \u2014 one per line", value: String(v.highlightsText ?? ""), onChange: (e) => onChange({ highlightsText: e.target.value }), rows: 4, placeholder: "Genuine Takara Tomy stock\nChecked and photographed before listing" }), _jsx(Text, { size: "xs", color: "muted", children: "Short \"why shop this\" bullets, shown above the FAQs. Blank lines are dropped. Up to 12." })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", children: "Category FAQs" }), _jsx(Text, { size: "xs", color: "muted", children: "Rendered as an accordion on the category page and emitted as FAQPage structured data. Keep platform-wide answers in the global FAQ collection instead \u2014 two copies of one answer means the one that gets fixed is never the one being read." }), _jsx(Repeater, { rows: faqs, onChange: setFaqs, blank: () => ({ question: "", answer: "" }), addLabel: "Add an FAQ", max: 20, renderRow: (row, set) => (_jsxs(_Fragment, { children: [_jsx(Input, { label: "Question", value: row.question, onChange: (e) => set({ question: e.target.value }), placeholder: "Are these compatible with Burst launchers?" }), _jsx(Textarea, { value: row.answer, onChange: (e) => set({ answer: e.target.value }), rows: 3, placeholder: "Answer\u2026" })] })) })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", children: "Description templates" }), _jsxs(Text, { size: "xs", color: "muted", children: ["What the seller form offers when a listing is filed here. One per condition band \u2014 worldhobbyshop reuses exactly two across 1,195 listings. Slots like", " ", _jsx(Span, { family: "mono", children: "{{title}}" }), ",", " ", _jsx(Span, { family: "mono", children: "{{brand}}" }), ",", " ", _jsx(Span, { family: "mono", children: "{{condition}}" }), " and", " ", _jsx(Span, { family: "mono", children: "{{series}}" }), " are filled from the listing; an unresolved one becomes empty and is reported to the seller, never left as a literal brace pair."] }), _jsx(Repeater, { rows: templates, onChange: setTemplates, blank: () => ({ id: "", label: "", variant: "default", body: "" }), addLabel: "Add a template", max: 8, renderRow: (row, set) => (_jsxs(_Fragment, { children: [_jsxs(Div, { layout: "grid", gap: "4", className: "sm:grid-cols-2", children: [_jsx(Input, { label: "Label", value: row.label, onChange: (e) => set({ label: e.target.value }), placeholder: "Pre-owned \u2014 condition as pictured" }), _jsx(Select, { label: "Condition band", value: row.variant, onChange: (e) => set({ variant: e.target.value }), options: TEMPLATE_VARIANT_OPTIONS })] }), _jsx(Textarea, { value: row.body, onChange: (e) => set({ body: e.target.value }), rows: 6, placeholder: "Important things to know about {{title}} \u2014 HTML permitted\u2026" }), _jsx(Text, { size: "xs", color: "muted", children: usedPlaceholders(row.body).length > 0
452
508
  ? `Slots used: ${usedPlaceholders(row.body).map((p) => `{{${p}}}`).join(", ")}`
453
509
  : "No slots used — this body will read identically on every listing." })] })) })] })] })),
454
510
  },
@@ -456,7 +512,7 @@ export function AdminCategoryEditorView({ categoryId, onSaved, onDeleted, embedd
456
512
  id: "defaults",
457
513
  label: "Product defaults",
458
514
  fields: ["taxCodeId", "weightG", "dimL", "dimW", "dimH", "seoTitleTemplate", "seoDescriptionTemplate"],
459
- render: ({ values: v, onChange, errors }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Text, { size: "xs", color: "muted", children: "What a listing filed under this category inherits. Written server-side when the product omits the field, and resolved PER FIELD up the ancestor chain \u2014 so a named-model leaf can supply its own features while the tax code and spec rows come from the line above it." }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", children: "Tax code" }), _jsx(PaginatedSelect, { value: String(v.taxCodeId ?? "") || null, onChange: (val) => onChange({ taxCodeId: val ?? "" }), loadOptions: loadTaxCodeOptions, placeholder: "Search tax codes\u2026", searchPlaceholder: "HSN or label\u2026", noResultsText: "No tax codes found", ariaLabel: "Tax code" }), _jsx(Text, { size: "xs", color: "muted", children: "\uD83D\uDED1 This is the GST mechanism, not a nicety. A listing whose category resolves no tax code falls back to a default rate and is flagged for triage \u2014 and the measure of success is that the fallback fires for nothing." })] }), _jsxs(Div, { layout: "grid", gap: "4", className: "sm:grid-cols-2", children: [_jsx(Select, { label: "Default condition", value: String(v.defaultCondition ?? ""), onChange: (e) => onChange({ defaultCondition: e.target.value }), options: CONDITION_DEFAULT_OPTIONS }), _jsx(Select, { label: "Default authenticity", value: String(v.defaultAuthenticity ?? ""), onChange: (e) => onChange({ defaultAuthenticity: e.target.value }), options: AUTHENTICITY_OPTIONS })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", children: "Specification rows" }), _jsx(Repeater, { rows: specs, onChange: setSpecs, blank: () => ({ name: "", value: "", unit: "" }), addLabel: "Add a specification", max: 20, renderRow: (row, set) => (_jsxs(Div, { layout: "grid", gap: "4", className: "sm:grid-cols-3", children: [_jsx(Input, { label: "Name", value: row.name, onChange: (e) => set({ name: e.target.value }), placeholder: "System" }), _jsx(Input, { label: "Value", value: row.value, onChange: (e) => set({ value: e.target.value }), placeholder: "Magne Core" }), _jsx(Input, { label: "Unit", value: row.unit ?? "", onChange: (e) => set({ unit: e.target.value }), placeholder: "g" })] })) })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", children: "What's in the box \u2014 one per line" }), _jsx(Textarea, { value: String(v.inTheBoxText ?? ""), onChange: (e) => onChange({ inTheBoxText: e.target.value }), rows: 4, placeholder: "Beyblade top\nLauncher\nRipcord\nManual" }), _jsx(Text, { size: "xs", color: "muted", children: "Read by the description-template endpoint to fill the new-in-box body's list. Deliberately NOT written to the product's `features` field, which holds feature ids \u2014 putting prose there would break every facet on the listing." })] }), _jsxs(Div, { layout: "grid", gap: "4", className: "sm:grid-cols-2", children: [_jsx(Input, { label: "SEO title template", value: String(v.seoTitleTemplate ?? ""), onChange: (e) => onChange({ seoTitleTemplate: e.target.value }), placeholder: "{{title}} \u2014 {{brand}} | {{siteName}}", error: errors.seoTitleTemplate }), _jsx(Input, { label: "SEO description template", value: String(v.seoDescriptionTemplate ?? ""), onChange: (e) => onChange({ seoDescriptionTemplate: e.target.value }), placeholder: "Buy {{title}} in India\u2026", error: errors.seoDescriptionTemplate })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", children: "SEO keywords \u2014 one per line" }), _jsx(Textarea, { value: String(v.seoKeywordsText ?? ""), onChange: (e) => onChange({ seoKeywordsText: e.target.value }), rows: 3 })] }), _jsxs(Div, { layout: "grid", gap: "4", className: "sm:grid-cols-4", children: [_jsx(Input, { label: "Packed weight (g)", value: String(v.weightG ?? ""), onChange: (e) => onChange({ weightG: e.target.value }), placeholder: "300", error: errors.weightG, helperText: "Boxed, not the bare item." }), _jsx(Input, { label: "Length (cm)", value: String(v.dimL ?? ""), onChange: (e) => onChange({ dimL: e.target.value }), error: errors.dimL }), _jsx(Input, { label: "Width (cm)", value: String(v.dimW ?? ""), onChange: (e) => onChange({ dimW: e.target.value }), error: errors.dimW }), _jsx(Input, { label: "Height (cm)", value: String(v.dimH ?? ""), onChange: (e) => onChange({ dimH: e.target.value }), error: errors.dimH })] }), _jsx(Text, { size: "xs", color: "muted", children: "\uD83D\uDED1 The weight is a PACKED weight \u2014 a bey is ~30 g and ~300 g boxed. This figure drives the shipping estimate the buyer pays, and the seller's booking is capped at what was collected, so an item weight here under-estimates every booking and the difference is absorbed silently. All three dimensions or none." }), _jsx(Toggle, { label: "Fragile by default", checked: v.fragile === true, onChange: (checked) => onChange({ fragile: checked }) })] })),
515
+ render: ({ values: v, onChange, errors }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Text, { size: "xs", color: "muted", children: "What a listing filed under this category inherits. Written server-side when the product omits the field, and resolved PER FIELD up the ancestor chain \u2014 so a named-model leaf can supply its own features while the tax code and spec rows come from the line above it." }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", children: "Tax code" }), _jsx(PaginatedSelect, { value: String(v.taxCodeId ?? "") || null, onChange: (val) => onChange({ taxCodeId: val ?? "" }), loadOptions: loadTaxCodeOptions, placeholder: "Search tax codes\u2026", searchPlaceholder: "HSN or label\u2026", noResultsText: "No tax codes found", ariaLabel: "Tax code" }), _jsx(Text, { size: "xs", color: "muted", children: "\uD83D\uDED1 This is the GST mechanism, not a nicety. A listing whose category resolves no tax code falls back to a default rate and is flagged for triage \u2014 and the measure of success is that the fallback fires for nothing." })] }), _jsxs(Div, { layout: "grid", gap: "4", className: "sm:grid-cols-2", children: [_jsx(Select, { label: "Default condition", value: String(v.defaultCondition ?? ""), onChange: (e) => onChange({ defaultCondition: e.target.value }), options: CONDITION_DEFAULT_OPTIONS }), _jsx(Select, { label: "Default authenticity", value: String(v.defaultAuthenticity ?? ""), onChange: (e) => onChange({ defaultAuthenticity: e.target.value }), options: AUTHENTICITY_OPTIONS })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", children: "Specification rows" }), _jsx(Repeater, { rows: specs, onChange: setSpecs, blank: () => ({ name: "", value: "", unit: "" }), addLabel: "Add a specification", max: 20, renderRow: (row, set) => (_jsxs(Div, { layout: "grid", gap: "4", className: "sm:grid-cols-3", children: [_jsx(Input, { label: "Name", value: row.name, onChange: (e) => set({ name: e.target.value }), placeholder: "System" }), _jsx(Input, { label: "Value", value: row.value, onChange: (e) => set({ value: e.target.value }), placeholder: "Magne Core" }), _jsx(Input, { label: "Unit", value: row.unit ?? "", onChange: (e) => set({ unit: e.target.value }), placeholder: "g" })] })) })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Textarea, { label: "What's in the box \u2014 one per line", value: String(v.inTheBoxText ?? ""), onChange: (e) => onChange({ inTheBoxText: e.target.value }), rows: 4, placeholder: "Beyblade top\nLauncher\nRipcord\nManual" }), _jsx(Text, { size: "xs", color: "muted", children: "Read by the description-template endpoint to fill the new-in-box body's list. Deliberately NOT written to the product's `features` field, which holds feature ids \u2014 putting prose there would break every facet on the listing." })] }), _jsxs(Div, { layout: "grid", gap: "4", className: "sm:grid-cols-2", children: [_jsx(Input, { label: "SEO title template", value: String(v.seoTitleTemplate ?? ""), onChange: (e) => onChange({ seoTitleTemplate: e.target.value }), placeholder: "{{title}} \u2014 {{brand}} | {{siteName}}", error: errors.seoTitleTemplate }), _jsx(Input, { label: "SEO description template", value: String(v.seoDescriptionTemplate ?? ""), onChange: (e) => onChange({ seoDescriptionTemplate: e.target.value }), placeholder: "Buy {{title}} in India\u2026", error: errors.seoDescriptionTemplate })] }), _jsx(Stack, { gap: "xs", children: _jsx(Textarea, { label: "SEO keywords \u2014 one per line", value: String(v.seoKeywordsText ?? ""), onChange: (e) => onChange({ seoKeywordsText: e.target.value }), rows: 3 }) }), _jsxs(Div, { layout: "grid", gap: "4", className: "sm:grid-cols-4", children: [_jsx(Input, { label: "Packed weight (g)", value: String(v.weightG ?? ""), onChange: (e) => onChange({ weightG: e.target.value }), placeholder: "300", error: errors.weightG, helperText: "Boxed, not the bare item." }), _jsx(Input, { label: "Length (cm)", value: String(v.dimL ?? ""), onChange: (e) => onChange({ dimL: e.target.value }), error: errors.dimL }), _jsx(Input, { label: "Width (cm)", value: String(v.dimW ?? ""), onChange: (e) => onChange({ dimW: e.target.value }), error: errors.dimW }), _jsx(Input, { label: "Height (cm)", value: String(v.dimH ?? ""), onChange: (e) => onChange({ dimH: e.target.value }), error: errors.dimH })] }), _jsx(Text, { size: "xs", color: "muted", children: "\uD83D\uDED1 The weight is a PACKED weight \u2014 a bey is ~30 g and ~300 g boxed. This figure drives the shipping estimate the buyer pays, and the seller's booking is capped at what was collected, so an item weight here under-estimates every booking and the difference is absorbed silently. All three dimensions or none." }), _jsx(Toggle, { label: "Fragile by default", checked: v.fragile === true, onChange: (checked) => onChange({ fragile: checked }) })] })),
460
516
  },
461
517
  ]
462
518
  : []),
@@ -29,7 +29,7 @@ export function AdminContactEditorView({ open, onClose, submissionId, subject, n
29
29
  });
30
30
  const statusColor = {
31
31
  new: "bg-info-surface text-info",
32
- read: "bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text-muted)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]",
32
+ read: "bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]",
33
33
  resolved: "bg-success-surface text-success",
34
34
  };
35
35
  return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: subject ?? "Contact Submission", children: _jsxs(Stack, { className: `${__P.p4}`, gap: "5", children: [_jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Text, { size: "xs", color: "muted", children: "Status:" }), _jsx(Span, { layout: "inline-flex", className: `${statusColor[currentStatus ?? "new"] ?? statusColor.new}`, size: "xs", weight: "medium", rounded: "full", padding: "pill-xs", children: currentStatus ?? "new" })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "tracking-wide", color: "muted", size: "xs", weight: "medium", transform: "uppercase", children: "From" }), _jsx(Text, { size: "sm", weight: "medium", color: "primary", children: name ?? "Unknown" }), email && (_jsx(Text, { size: "sm", color: "muted", children: email }))] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "tracking-wide", color: "muted", size: "xs", weight: "medium", transform: "uppercase", children: "Message" }), _jsx(Div, { className: `${__P.p3} max-h-64 ${__O.yAuto}`, rounded: "lg", surface: "muted", border: "default", children: _jsx(Text, { className: "whitespace-pre-wrap leading-relaxed", color: "primary", size: "sm", children: message ?? "No message body." }) })] }), _jsxs(FormActions, { align: "right", children: [email && (_jsx(Button, { type: "button", variant: "outline", onClick: () => {
@@ -4,7 +4,8 @@ import { normalizeError } from "../../../errors/normalize.js";
4
4
  import { useApiMutation } from "@mohasinac/appkit/client";
5
5
  import React from "react";
6
6
  import { useQuery } from "@tanstack/react-query";
7
- import { ConfirmDeleteModal, Div, StackedViewShell, useToast } from "../../../ui/index.js";
7
+ import { ConfirmDeleteModal, Div, Input, StackedViewShell, useToast } from "../../../ui/index.js";
8
+ import { Repeater } from "../../../ui/components/Repeater.js";
8
9
  import { FieldInput, FormErrorSummary } from "../../../ui/forms/index.js";
9
10
  import { apiClient } from "../../../http/index.js";
10
11
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
@@ -63,6 +64,7 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
63
64
  isActive: true,
64
65
  firstTimeUserOnly: false,
65
66
  appliesToAuctions: false,
67
+ tiers: [],
66
68
  applicableProducts: [],
67
69
  applicableCategories: [],
68
70
  });
@@ -113,6 +115,7 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
113
115
  ? c.restrictions.applicableCategories
114
116
  : [],
115
117
  appliesToAuctions: c.applicableToAuctions ?? false,
118
+ tiers: c.tiers ?? [],
116
119
  });
117
120
  }, [couponQuery.data]);
118
121
  const handleNameChange = (value) => {
@@ -166,6 +169,13 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
166
169
  }),
167
170
  },
168
171
  applicableToAuctions: form.appliesToAuctions,
172
+ /*
173
+ * 🛑 Sent even when EMPTY so that clearing the last tier persists —
174
+ * the same reasoning as the brand highlights. A tier with no
175
+ * threshold and no value is dropped rather than stored: it would be
176
+ * a band that matches every order and discounts nothing.
177
+ */
178
+ tiers: form.tiers.filter((t) => t.minAmount > 0 || t.discountValue > 0),
169
179
  };
170
180
  if (isEdit) {
171
181
  return apiClient.patch(ADMIN_ENDPOINTS.COUPON_BY_ID(couponId), payload);
@@ -208,6 +218,21 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
208
218
  const sections = React.useMemo(() => buildSectionsFromSchema(adminCouponFormSchema, {
209
219
  options: { type: TYPE_OPTIONS.map((o) => ({ value: String(o.value), label: o.label })) },
210
220
  renderers: {
221
+ /*
222
+ * Spend tiers. `Repeater` is the one promoted out of
223
+ * `AdminCategoryEditorView`; this is its fifth caller.
224
+ *
225
+ * 🛑 Both inputs are NUMBERS, parsed here rather than kept as
226
+ * strings, because `getTieredDiscount` sorts on `minAmount` and
227
+ * compares it to the order total. A string "1000" sorts before
228
+ * "900" lexically, which would silently pick the wrong band.
229
+ */
230
+ tiers: ({ values, onChange }) => {
231
+ const rows = values.tiers ?? [];
232
+ return (_jsx(Repeater, { rows: rows, onChange: (next) => onChange({ tiers: next }), blank: () => ({ minAmount: 0, discountValue: 0 }), addLabel: "Add a tier", max: 6, renderRow: (row, set) => (_jsxs(Div, { layout: "grid", gap: "4", className: "sm:grid-cols-2", children: [_jsx(Input, { label: "Spend at least (\u20B9)", type: "number", min: 0, value: String(row.minAmount), onChange: (e) => set({ minAmount: Number(e.target.value) || 0 }) }), _jsx(Input, { label: values.type === "percentage"
233
+ ? "Discount (%)"
234
+ : "Discount (₹)", type: "number", min: 0, value: String(row.discountValue), onChange: (e) => set({ discountValue: Number(e.target.value) || 0 }) })] })) }));
235
+ },
211
236
  // Typing the campaign name fills the code until the admin edits it.
212
237
  name: ({ values, errors }) => (_jsx(FieldInput, { name: "name", label: "Campaign name", required: true, value: values.name, error: errors.name, onChange: handleNameChange })),
213
238
  code: ({ values, onChange, errors }) => (_jsx(FieldInput, { name: "code", label: "Coupon code", required: true, disabled: isEdit, hint: isEdit ? "A code cannot change once customers have it." : undefined, value: values.code, error: errors.code, onChange: (v) => {
@@ -187,7 +187,7 @@ export function AdminFaqEditorView({ faqId, onSaved, onDeleted, embedded, ...res
187
187
  render: ({ values: v, onChange, errors }) => (_jsxs(Stack, { gap: "md", children: [_jsx(FieldInput, { name: "question", label: "Question", value: String(v.question ?? ""), onChange: (val) => onChange({ question: val }), required: true, placeholder: "e.g. How does bidding work on LetItRip?", error: errors.question }), _jsx(Input, { label: "Slug", value: String(v.slug ?? ""), onChange: (e) => {
188
188
  setSlugManual(true);
189
189
  onChange({ slug: e.target.value });
190
- }, placeholder: "faq-how-does-bidding-work", helperText: "Auto-generated from the question until you edit it. Must start with 'faq-'.", error: errors.slug }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Answer" }), _jsx(RichTextEditor, { value: String(v.answer ?? ""), onChange: (val) => onChange({ answer: val }), placeholder: "Write a clear, helpful answer...", minHeightClassName: "min-h-[200px]" }), errors.answer && (_jsx(Text, { size: "xs", color: "error", role: "alert", children: errors.answer }))] })] })),
190
+ }, placeholder: "faq-how-does-bidding-work", helperText: "Auto-generated from the question until you edit it. Must start with 'faq-'.", error: errors.slug }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Answer" }), _jsx(RichTextEditor, { ariaLabel: "Answer", value: String(v.answer ?? ""), onChange: (val) => onChange({ answer: val }), placeholder: "Write a clear, helpful answer...", minHeightClassName: "min-h-[200px]" }), errors.answer && (_jsx(Text, { size: "xs", color: "error", role: "alert", children: errors.answer }))] })] })),
191
191
  },
192
192
  {
193
193
  id: "classification",