@mohasinac/appkit 4.61.0 → 4.62.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 (281) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +19 -3
  3. package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +1 -1
  4. package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +1 -1
  5. package/dist/_internal/server/features/checkout/actions.d.ts +80 -0
  6. package/dist/_internal/server/features/checkout/actions.js +542 -26
  7. package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
  8. package/dist/_internal/server/features/invoicing/allocate.js +162 -0
  9. package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
  10. package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
  11. package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
  12. package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
  13. package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
  14. package/dist/_internal/server/features/invoicing/issue.js +202 -0
  15. package/dist/_internal/server/features/orders/actions.d.ts +8 -1
  16. package/dist/_internal/server/features/orders/actions.js +81 -8
  17. package/dist/_internal/server/features/orders/adapters.js +33 -0
  18. package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
  19. package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
  20. package/dist/_internal/server/features/points/award.js +46 -4
  21. package/dist/_internal/server/features/refunds/actions.js +26 -0
  22. package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
  23. package/dist/_internal/server/features/shipping/courier-block.js +239 -0
  24. package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
  25. package/dist/_internal/server/features/site-settings/adapters.js +15 -0
  26. package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
  27. package/dist/_internal/server/features/stores/adapters.js +51 -0
  28. package/dist/_internal/server/functions/scheduled.js +38 -1
  29. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +9 -2
  30. package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
  31. package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
  32. package/dist/_internal/server/jobs/core/index.d.ts +2 -1
  33. package/dist/_internal/server/jobs/core/index.js +2 -1
  34. package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
  35. package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
  36. package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
  37. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +62 -0
  38. package/dist/_internal/server/jobs/core/paymentReconcile.js +87 -97
  39. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
  40. package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
  41. package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
  42. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +10 -2
  43. package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
  44. package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
  45. package/dist/_internal/shared/actions/action-registry.js +177 -9
  46. package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
  47. package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
  48. package/dist/_internal/shared/checkout/order-math.d.ts +145 -0
  49. package/dist/_internal/shared/checkout/order-math.js +173 -0
  50. package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
  51. package/dist/_internal/shared/checkout/refundable-base.js +13 -1
  52. package/dist/_internal/shared/checkout/split.d.ts +87 -0
  53. package/dist/_internal/shared/checkout/split.js +100 -0
  54. package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
  55. package/dist/_internal/shared/content/disclaimers.js +217 -0
  56. package/dist/_internal/shared/content/published-values.d.ts +88 -0
  57. package/dist/_internal/shared/content/published-values.js +198 -0
  58. package/dist/_internal/shared/features/admin/integrations.d.ts +109 -0
  59. package/dist/_internal/shared/features/admin/integrations.js +253 -0
  60. package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
  61. package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
  62. package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
  63. package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
  64. package/dist/_internal/shared/features/points/accepts.d.ts +36 -0
  65. package/dist/_internal/shared/features/points/accepts.js +43 -0
  66. package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
  67. package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
  68. package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
  69. package/dist/_internal/shared/fees/calculator.d.ts +33 -1
  70. package/dist/_internal/shared/fees/calculator.js +32 -2
  71. package/dist/_internal/shared/media/limits.d.ts +10 -0
  72. package/dist/_internal/shared/media/limits.js +10 -0
  73. package/dist/_internal/shared/types/action-result.js +15 -1
  74. package/dist/client/api/surface-error.js +16 -2
  75. package/dist/client/api/useApiMutation.js +17 -1
  76. package/dist/client.d.ts +12 -1
  77. package/dist/client.js +31 -1
  78. package/dist/constants/api-endpoints.d.ts +45 -0
  79. package/dist/constants/api-endpoints.js +15 -0
  80. package/dist/constants/field-names.d.ts +18 -0
  81. package/dist/constants/field-names.js +28 -0
  82. package/dist/contracts/index.d.ts +1 -1
  83. package/dist/contracts/shipping.d.ts +45 -0
  84. package/dist/contracts/shipping.js +15 -0
  85. package/dist/core/server-action.js +19 -3
  86. package/dist/errors/error-mapping.js +26 -1
  87. package/dist/features/about/components/AboutView.js +1 -1
  88. package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
  89. package/dist/features/account/components/UserOffersPanel.js +5 -3
  90. package/dist/features/account/components/UserOrdersView.js +19 -33
  91. package/dist/features/admin/components/AdminActionIndexView.js +100 -5
  92. package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
  93. package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
  94. package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
  95. package/dist/features/admin/components/AdminCarouselView.js +96 -4
  96. package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
  97. package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
  98. package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
  99. package/dist/features/admin/components/AdminInvoicingView.js +65 -0
  100. package/dist/features/admin/components/AdminMediaView.js +36 -3
  101. package/dist/features/admin/components/AdminNavigationView.js +4 -1
  102. package/dist/features/admin/components/AdminOffersView.js +2 -1
  103. package/dist/features/admin/components/AdminOrderEditorView.d.ts +18 -1
  104. package/dist/features/admin/components/AdminOrderEditorView.js +11 -2
  105. package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
  106. package/dist/features/admin/components/AdminOrdersView.js +67 -5
  107. package/dist/features/admin/components/AdminPaymentClustersView.js +19 -2
  108. package/dist/features/admin/components/AdminProductEditorView.js +2 -2
  109. package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
  110. package/dist/features/admin/components/AdminSectionsView.js +486 -224
  111. package/dist/features/admin/components/OwnerQuickActions.js +24 -2
  112. package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
  113. package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
  114. package/dist/features/admin/constants/filter-tabs.js +6 -0
  115. package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
  116. package/dist/features/admin/hooks/useAdminListingData.js +16 -0
  117. package/dist/features/admin/hooks/useOptimisticRowFlag.d.ts +33 -0
  118. package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
  119. package/dist/features/admin/repository/notification.repository.d.ts +15 -0
  120. package/dist/features/admin/repository/notification.repository.js +32 -0
  121. package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
  122. package/dist/features/admin/schemas/action-index-form.js +115 -0
  123. package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
  124. package/dist/features/admin/schemas/firestore.d.ts +30 -0
  125. package/dist/features/admin/schemas/firestore.js +5 -0
  126. package/dist/features/audit-log/schemas/firestore.d.ts +30 -0
  127. package/dist/features/audit-log/schemas/firestore.js +15 -0
  128. package/dist/features/auth/components/RegisterForm.js +2 -1
  129. package/dist/features/auth/permissions/constants.js +13 -0
  130. package/dist/features/cart/components/CartPriceBreakdown.d.ts +13 -0
  131. package/dist/features/cart/components/CartPriceBreakdown.js +6 -2
  132. package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
  133. package/dist/features/cart/components/PointsTenderPicker.js +15 -0
  134. package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
  135. package/dist/features/cart/components/index.d.ts +2 -0
  136. package/dist/features/cart/components/index.js +1 -0
  137. package/dist/features/cart/repository/cart.repository.d.ts +19 -0
  138. package/dist/features/cart/repository/cart.repository.js +27 -0
  139. package/dist/features/cart/schemas/firestore.d.ts +92 -0
  140. package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
  141. package/dist/features/contact/email.d.ts +27 -10
  142. package/dist/features/contact/email.js +20 -3
  143. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +38 -0
  144. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +80 -0
  145. package/dist/features/dispute-reasons/schemas/firestore.d.ts +64 -0
  146. package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
  147. package/dist/features/events/components/AdminEventEditorView.js +8 -6
  148. package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
  149. package/dist/features/faq/components/FAQAccordion.js +29 -4
  150. package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
  151. package/dist/features/invoicing/schemas/firestore.js +45 -0
  152. package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
  153. package/dist/features/invoicing/utils/financial-year.js +53 -0
  154. package/dist/features/jobs/actions/enqueue-job.js +18 -1
  155. package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
  156. package/dist/features/jobs/repository/jobs.repository.js +84 -1
  157. package/dist/features/jobs/schemas/firestore.d.ts +80 -0
  158. package/dist/features/jobs/schemas/firestore.js +37 -0
  159. package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
  160. package/dist/features/lottery/schemas/zod.d.ts +6 -6
  161. package/dist/features/media/MediaImage.js +22 -1
  162. package/dist/features/media/upload/ImageUpload.js +1 -1
  163. package/dist/features/obligations/constants/policies.js +24 -1
  164. package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
  165. package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
  166. package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
  167. package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
  168. package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
  169. package/dist/features/orders/components/RaiseDisputeForm.js +111 -0
  170. package/dist/features/orders/components/RefundRequestView.js +22 -4
  171. package/dist/features/orders/components/index.d.ts +4 -0
  172. package/dist/features/orders/components/index.js +2 -0
  173. package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
  174. package/dist/features/orders/constants/order-lane-tabs.js +88 -0
  175. package/dist/features/orders/repository/orders.repository.d.ts +57 -0
  176. package/dist/features/orders/repository/orders.repository.js +145 -0
  177. package/dist/features/orders/schemas/firestore.d.ts +196 -1
  178. package/dist/features/orders/schemas/index.d.ts +60 -12
  179. package/dist/features/orders/schemas/index.js +12 -0
  180. package/dist/features/orders/types/index.d.ts +26 -0
  181. package/dist/features/orders/utils/courier-block.d.ts +25 -0
  182. package/dist/features/orders/utils/courier-block.js +23 -0
  183. package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
  184. package/dist/features/payment-cases/schemas/firestore.js +1 -0
  185. package/dist/features/products/components/GroupMemberPicker.js +4 -1
  186. package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
  187. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
  188. package/dist/features/products/components/ProductDetailActions.js +2 -1
  189. package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
  190. package/dist/features/products/components/ProductDetailPageView.js +2 -2
  191. package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
  192. package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
  193. package/dist/features/promotions/actions/coupon-stacking.js +21 -2
  194. package/dist/features/promotions/components/CouponCard.js +13 -0
  195. package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
  196. package/dist/features/promotions/constants/coupon-help.js +9 -1
  197. package/dist/features/promotions/schemas/firestore.d.ts +24 -0
  198. package/dist/features/promotions/schemas/firestore.js +4 -0
  199. package/dist/features/promotions/types/index.d.ts +19 -1
  200. package/dist/features/scams/components/ScamProfileView.js +2 -2
  201. package/dist/features/scams/components/ScamRegistryView.js +2 -2
  202. package/dist/features/search/action-index/tab-entries.js +1 -1
  203. package/dist/features/seller/actions/seller-actions.js +33 -3
  204. package/dist/features/seller/components/FulfillmentView.js +3 -2
  205. package/dist/features/seller/components/SellerAddressesView.js +32 -4
  206. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
  207. package/dist/features/seller/components/SellerBundlesView.js +53 -14
  208. package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
  209. package/dist/features/seller/components/SellerListingTypeView.js +56 -10
  210. package/dist/features/seller/components/SellerOrdersView.js +129 -27
  211. package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
  212. package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
  213. package/dist/features/seller/components/SellerProductsView.js +5 -1
  214. package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
  215. package/dist/features/seller/components/SellerShippingView.js +2 -1
  216. package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
  217. package/dist/features/seller/components/SellerSupportView.js +69 -2
  218. package/dist/features/seller/hooks/useSellerListingData.js +25 -0
  219. package/dist/features/shell/SectionForm.js +20 -16
  220. package/dist/features/shell/build-sections.js +14 -1
  221. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
  222. package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
  223. package/dist/features/shipments/schemas/validation.d.ts +4 -4
  224. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +43 -0
  225. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +90 -0
  226. package/dist/features/shipping-boxes/schemas/firestore.d.ts +82 -0
  227. package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
  228. package/dist/features/sticker-trace/components/StickerCommitPanel.js +2 -1
  229. package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
  230. package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
  231. package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
  232. package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
  233. package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
  234. package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
  235. package/dist/features/stores/schemas/firestore.d.ts +71 -0
  236. package/dist/features/stores/types/index.d.ts +19 -0
  237. package/dist/features/support/schemas/index.d.ts +6 -6
  238. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
  239. package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
  240. package/dist/index.d.ts +11 -2
  241. package/dist/index.js +17 -2
  242. package/dist/next/routing/route-map.d.ts +24 -0
  243. package/dist/next/routing/route-map.js +19 -0
  244. package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
  245. package/dist/providers/shipping-shiprocket/index.js +84 -1
  246. package/dist/schemas/registry.d.ts +51 -22
  247. package/dist/schemas/sieve.d.ts +5 -5
  248. package/dist/server-entry.d.ts +2 -2
  249. package/dist/server-entry.js +8 -1
  250. package/dist/server.d.ts +11 -2
  251. package/dist/server.js +40 -2
  252. package/dist/styles.css +64 -80
  253. package/dist/tailwind-utilities.css +1 -1
  254. package/dist/tokens/themes/default-dark.js +1 -0
  255. package/dist/tokens/themes/default-light.js +1 -0
  256. package/dist/tokens/themes/required.d.ts +1 -1
  257. package/dist/tokens/themes/required.js +1 -0
  258. package/dist/tokens/tokens.css +15 -0
  259. package/dist/ui/components/Checkbox.js +10 -2
  260. package/dist/ui/components/DateInput.js +8 -1
  261. package/dist/ui/components/Disclaimer.d.ts +19 -0
  262. package/dist/ui/components/Disclaimer.js +74 -0
  263. package/dist/ui/components/Input.js +22 -2
  264. package/dist/ui/components/PaginatedSelect.d.ts +8 -19
  265. package/dist/ui/components/PaginatedSelect.js +43 -9
  266. package/dist/ui/components/Radio.js +8 -1
  267. package/dist/ui/components/Select.js +10 -2
  268. package/dist/ui/components/TagInput.d.ts +8 -1
  269. package/dist/ui/components/TagInput.js +4 -3
  270. package/dist/ui/components/Textarea.js +10 -2
  271. package/dist/ui/components/tabs-overflow.js +64 -11
  272. package/dist/ui/forms/FieldCheckbox.js +1 -1
  273. package/dist/ui/forms/FormErrorList.d.ts +0 -16
  274. package/dist/ui/forms/FormErrorList.js +34 -1
  275. package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
  276. package/dist/ui/forms/apply-zod-issues.js +23 -1
  277. package/dist/ui/index.d.ts +2 -0
  278. package/dist/ui/index.js +6 -0
  279. package/dist/ui/rich-text/RichText.style.css +48 -79
  280. package/package.json +1 -1
  281. package/scripts/seed-cli.mjs +1831 -1822
@@ -10,6 +10,8 @@ import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
10
10
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
11
11
  import { ROUTES } from "../../../constants/index.js";
12
12
  import { toRecordArray, toStringValue } from "../hooks/useSellerListingData.js";
13
+ import { useOptimisticRowFlag } from "../../admin/hooks/useOptimisticRowFlag.js";
14
+ import { apiClient } from "../../../http/index.js";
13
15
  import { DataListingView } from "../../admin/components/DataListingView.js";
14
16
  const DEFAULT_SORT = "-createdAt";
15
17
  const SORT_OPTIONS = [
@@ -40,6 +42,22 @@ const COLUMNS = [
40
42
  },
41
43
  ];
42
44
  export function SellerShippingConfigsView({ onCreateClick, onEditClick, onDelete, onSetDefault, }) {
45
+ /*
46
+ * 🛑 `isActive` was a READ-ONLY badge here, and the update schema was built
47
+ * to take it on its own — shipping-config's even says so in a comment: "a
48
+ * PATCH may legitimately carry only `isActive`". Nothing in the client had
49
+ * ever sent one, so flipping one boolean meant a full-editor round trip.
50
+ *
51
+ * The optimistic/rollback logic lives in the hook because eight listings
52
+ * grew this control at once; see its header for why rollback must DELETE
53
+ * the override key rather than write back a captured value.
54
+ */
55
+ const activeFlag = useOptimisticRowFlag({
56
+ mutate: (id, next) => apiClient.patch(SELLER_ENDPOINTS.SHIPPING_CONFIG_BY_ID(id), { isActive: next }),
57
+ invalidate: [["seller", "shipping-configs", "listing"]],
58
+ successMessage: (next) => (next ? "Shipping option activated." : "Shipping option deactivated."),
59
+ errorMessage: "Could not change that shipping option's status.",
60
+ });
43
61
  const [deleteTargetId, setDeleteTargetId] = useState(null);
44
62
  const { deletingId, handleDelete: performDelete } = useEntityDelete({
45
63
  endpoint: SELLER_ENDPOINTS.SHIPPING_CONFIG_BY_ID,
@@ -113,12 +131,26 @@ export function SellerShippingConfigsView({ onCreateClick, onEditClick, onDelete
113
131
  method: String(item.method ?? ""),
114
132
  estimatedDays: typeof item.estimatedDays === "number" ? item.estimatedDays : null,
115
133
  isDefault: Boolean(item.isDefault),
116
- isActive: Boolean(item.isActive),
134
+ // Resolved through the pending override so the Status badge and the
135
+ // row toggle cannot disagree between the click and the refetch.
136
+ isActive: activeFlag.resolve(toStringValue(item.id, `sc-${index}`), Boolean(item.isActive)),
117
137
  })),
118
138
  getTotal: (response, mappedRows) => (typeof response.total === "number" ? response.total : mappedRows.length),
119
139
  buildFilters: () => undefined,
120
140
  primaryAction: { label: "New Config", onClick: handleCreate },
121
141
  renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
142
+ /*
143
+ * 🛑 `isActive` was a read-only badge and the update schema was
144
+ * built to accept it alone. A menu entry rather than a switch here,
145
+ * to match the other actions in this menu — the menu is already
146
+ * click-stopped in all three renderers.
147
+ */
148
+ {
149
+ label: row.isActive ? "Deactivate" : "Activate",
150
+ separator: true,
151
+ disabled: activeFlag.pendingId === row.id,
152
+ onClick: () => { void activeFlag.set(row.id, !row.isActive); },
153
+ },
122
154
  { label: ACTIONS.STORE["edit-listing"].label, onClick: () => handleEdit(row.id) },
123
155
  ...(!row.isDefault
124
156
  ? [{ label: "Set as Default", disabled: settingDefaultId === row.id, onClick: () => handleSetDefault(row.id) }]
@@ -9,6 +9,7 @@ import { StoreAddressSelectorCreate } from "../../stores/components/StoreAddress
9
9
  import { SectionForm, useSectionFormNav } from "../../shell/index.js";
10
10
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
11
11
  import { normalizeError } from "../../../errors/normalize.js";
12
+ import { toUserMessage } from "../../../errors/error-display-map.js";
12
13
  const shippingDraftSchema = z.object({
13
14
  customCarrierName: z.string().min(1, "Carrier name is required"),
14
15
  customShippingPrice: z.string(),
@@ -82,7 +83,7 @@ export function SellerShippingView({ apiBase = SELLER_ENDPOINTS.SHIPPING }) {
82
83
  if (Array.isArray(json?.issues) && json.issues.length > 0) {
83
84
  applyZodIssues(json.issues, setFieldError);
84
85
  }
85
- setError(json?.error ?? "Failed to save");
86
+ setError(toUserMessage(json?.code, undefined, { fallback: "We couldn't save your shipping settings." }));
86
87
  return;
87
88
  }
88
89
  clearErrors();
@@ -11,6 +11,8 @@ import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
11
11
  import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers.js";
12
12
  import { ROUTES } from "../../../next/routing/route-map.js";
13
13
  import { toRecordArray, toStringValue } from "../hooks/useSellerListingData.js";
14
+ import { useOptimisticRowFlag } from "../../admin/hooks/useOptimisticRowFlag.js";
15
+ import { apiClient } from "../../../http/index.js";
14
16
  import { DataListingView } from "../../admin/components/DataListingView.js";
15
17
  const DEFAULT_SORT = "displayOrder";
16
18
  const SORT_OPTIONS = [
@@ -37,6 +39,22 @@ const COLUMNS = [
37
39
  },
38
40
  ];
39
41
  export function SellerStoreCategoriesView({ onCreateClick, onEditClick, onDelete, onBulkDelete, onBulkActivate, onBulkDeactivate, }) {
42
+ /*
43
+ * 🛑 `isActive` was a READ-ONLY badge here, and the update schema was built
44
+ * to take it on its own — shipping-config's even says so in a comment: "a
45
+ * PATCH may legitimately carry only `isActive`". Nothing in the client had
46
+ * ever sent one, so flipping one boolean meant a full-editor round trip.
47
+ *
48
+ * The optimistic/rollback logic lives in the hook because eight listings
49
+ * grew this control at once; see its header for why rollback must DELETE
50
+ * the override key rather than write back a captured value.
51
+ */
52
+ const activeFlag = useOptimisticRowFlag({
53
+ mutate: (id, next) => apiClient.patch(SELLER_ENDPOINTS.STORE_CATEGORY_BY_ID(id), { isActive: next }),
54
+ invalidate: [["seller", "store-categories", "listing"]],
55
+ successMessage: (next) => (next ? "Category activated." : "Category deactivated."),
56
+ errorMessage: "Could not change that category's status.",
57
+ });
40
58
  const { showToast } = useToast();
41
59
  const [deleteTargetId, setDeleteTargetId] = useState(null);
42
60
  const { deletingId, handleDelete: performDelete } = useEntityDelete({
@@ -79,7 +97,9 @@ export function SellerStoreCategoriesView({ onCreateClick, onEditClick, onDelete
79
97
  label: String(item.label ?? ""),
80
98
  slug: String(item.slug ?? ""),
81
99
  productCount: Number(item.productIds?.length ?? 0),
82
- isActive: Boolean(item.isActive),
100
+ // Resolved through the pending override so the Status badge and the
101
+ // row toggle cannot disagree between the click and the refetch.
102
+ isActive: activeFlag.resolve(toStringValue(item.id, `cat-${index}`), Boolean(item.isActive)),
83
103
  })),
84
104
  getTotal: (response, rows) => (typeof response.total === "number" ? response.total : rows.length),
85
105
  buildFilters: () => undefined,
@@ -93,6 +113,18 @@ export function SellerStoreCategoriesView({ onCreateClick, onEditClick, onDelete
93
113
  return actions;
94
114
  },
95
115
  renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
116
+ /*
117
+ * 🛑 `isActive` was a read-only badge and the update schema was
118
+ * built to accept it alone. A menu entry rather than a switch here,
119
+ * to match the other actions in this menu — the menu is already
120
+ * click-stopped in all three renderers.
121
+ */
122
+ {
123
+ label: row.isActive ? "Deactivate" : "Activate",
124
+ separator: true,
125
+ disabled: activeFlag.pendingId === row.id,
126
+ onClick: () => { void activeFlag.set(row.id, !row.isActive); },
127
+ },
96
128
  {
97
129
  label: ACTIONS.STORE["edit-listing"].label,
98
130
  onClick: () => onEditClick
@@ -24,7 +24,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
24
24
  * close a complaint made against them.
25
25
  */
26
26
  import React from "react";
27
- import { Badge, Span, Stack, Text } from "../../../ui/index.js";
27
+ import { useQuery } from "@tanstack/react-query";
28
+ import { Badge, RecordDetailModal, Span, Stack, Text } from "../../../ui/index.js";
29
+ import { apiClient } from "../../../http/index.js";
28
30
  import { DataListingView } from "../../admin/components/DataListingView.js";
29
31
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
30
32
  import { SUPPORT_TICKET_FIELDS } from "../../../constants/field-names.js";
@@ -38,7 +40,58 @@ const STATUS_VARIANT = {
38
40
  resolved: "success",
39
41
  closed: "default",
40
42
  };
43
+ /**
44
+ * One ticket, read-only, in the shared detail modal.
45
+ *
46
+ * Its own component so the query only runs once a row has actually been
47
+ * opened — the list is the common case and nobody should pay a per-ticket
48
+ * fetch to scroll it.
49
+ */
50
+ function SellerSupportTicketModal({ ticketId, onClose, }) {
51
+ const { data, isLoading } = useQuery({
52
+ queryKey: ["store", "support-ticket", ticketId],
53
+ queryFn: () => apiClient.get(SELLER_ENDPOINTS.SUPPORT_TICKET_BY_ID(ticketId)),
54
+ staleTime: 30000,
55
+ });
56
+ const ticket = data?.ticket;
57
+ const messages = Array.isArray(ticket?.messages)
58
+ ? ticket.messages
59
+ : [];
60
+ return (_jsx(RecordDetailModal, { isOpen: true, onClose: onClose, title: toStringValue(ticket?.subject, isLoading ? "Loading…" : "Ticket"), badges: ticket
61
+ ? [
62
+ {
63
+ label: toStringValue(ticket.status).replace(/_/g, " "),
64
+ variant: STATUS_VARIANT[toStringValue(ticket.status)] ?? "default",
65
+ },
66
+ { label: toStringValue(ticket.category).replace(/_/g, " "), variant: "default" },
67
+ ]
68
+ : undefined, description: toStringValue(ticket?.description), fields: ticket
69
+ ? [
70
+ { label: "Ticket", value: toStringValue(ticket.id) },
71
+ { label: "Raised", value: toRelativeDate(ticket.createdAt) },
72
+ { label: "Last update", value: toRelativeDate(ticket.updatedAt ?? ticket.createdAt) },
73
+ { label: "Priority", value: toStringValue(ticket.priority, "—") },
74
+ ]
75
+ : undefined, items: messages.length > 0
76
+ ? {
77
+ heading: `Thread (${messages.length})`,
78
+ entries: messages.map((m) => ({
79
+ /*
80
+ * The author's ROLE, not their name. A seller reading a
81
+ * complaint made against them does not need the buyer's
82
+ * identity to follow the thread, and those fields are
83
+ * PII-encrypted at rest precisely so they are not handed out
84
+ * by default.
85
+ */
86
+ title: m.authorRole === "admin" ? "Support team" : "Buyer",
87
+ subtitle: toStringValue(m.body),
88
+ trailing: toRelativeDate((m.createdAt ?? null)),
89
+ })),
90
+ }
91
+ : undefined }));
92
+ }
41
93
  export function SellerSupportView({ labels = {} }) {
94
+ const [detailId, setDetailId] = React.useState(null);
42
95
  const mapRows = React.useCallback((response) => {
43
96
  const items = Array.isArray(response?.items) ? response.items : [];
44
97
  return items.map((raw) => {
@@ -96,5 +149,19 @@ export function SellerSupportView({ labels = {} }) {
96
149
  },
97
150
  { key: "updated", header: "Updated" },
98
151
  ],
99
- } }), _jsx(Text, { size: "xs", color: "muted", children: "Replies and status changes happen in the ticket thread with our support team \u2014 a ticket raised about your store is not one you can close." })] }));
152
+ /*
153
+ * 🛑 This list was a DEAD END, and unlike most of the ones fixed
154
+ * alongside it that was not a pure UI gap: a seller could read a
155
+ * subject and a 140-character slice of the description and never
156
+ * open the thread, under the footer below promising the thread is
157
+ * where replies happen. `/api/store/support` was GET-list-only with
158
+ * no `[id]` sibling at all, so the route had to be built first.
159
+ *
160
+ * A modal rather than a page: the seller cannot act on these — a
161
+ * ticket raised ABOUT a store is not one its owner can reply to or
162
+ * close — so there is nothing here a route would add, and this is
163
+ * the same `RecordDetailModal` the other dead ends were fixed with.
164
+ */
165
+ onRowClick: (row) => setDetailId(row.id),
166
+ } }), detailId && (_jsx(SellerSupportTicketModal, { ticketId: detailId, onClose: () => setDetailId(null) })), _jsx(Text, { size: "xs", color: "muted", children: "Replies and status changes happen in the ticket thread with our support team \u2014 a ticket raised about your store is not one you can close." })] }));
100
167
  }
@@ -1,10 +1,35 @@
1
1
  "use client";
2
2
  import { formatCurrency } from "../../../utils/number.formatter.js";
3
+ // The defining module, not a barrel (Root Cause #18). `hasEncPrefix` is a pure
4
+ // string test with no crypto in its import chain, so it is client-safe.
5
+ import { hasEncPrefix } from "../../../security/pii-mask.js";
3
6
  export function toRecordArray(value) {
4
7
  return Array.isArray(value) ? value.filter(Boolean) : [];
5
8
  }
6
9
  export function toStringValue(value, fallback = "-") {
7
10
  if (typeof value === "string") {
11
+ /*
12
+ * 🛑 Ciphertext is not a display value, and this is the last line of
13
+ * defence rather than the fix.
14
+ *
15
+ * `OrderRepository.mapDoc` deliberately does not decrypt, so any route
16
+ * that forgets `revealOrders`/`maskedOrders` hands this function a stored
17
+ * `enc:v1:…` blob — and it is a `string`, so it rendered verbatim.
18
+ * Measured live in the seller's order list, where the row subtitle read
19
+ * `enc:v1:8plr3ZzKBqejPPti:MCZsrfOidLkSL6EogbxlCzU=:…` where the buyer's
20
+ * name belongs.
21
+ *
22
+ * This is a RECURRING class, not one slip: eight separate files in appkit
23
+ * carry a comment recording the same leak, and one of them had already
24
+ * baked `enc:v1:…` into 383 notification bodies. The route fix is the real
25
+ * repair; this makes the NEXT one degrade to "-" instead of garbage.
26
+ *
27
+ * `hasEncPrefix` rather than `isPiiEncrypted`: its own docstring says this
28
+ * is the check the display masks want, because a settings-encrypted or
29
+ * malformed envelope is exactly as unshowable as a PII one.
30
+ */
31
+ if (hasEncPrefix(value))
32
+ return fallback;
8
33
  return value.trim() || fallback;
9
34
  }
10
35
  if (typeof value === "number" || typeof value === "boolean") {
@@ -325,6 +325,25 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
325
325
  }
326
326
  void onSubmit();
327
327
  }, [shellCtx, onSubmit, erroringSectionIds, fieldErrors, openIds, setOpenIds, expandMode]);
328
+ /**
329
+ * Open a section if it is closed, then scroll to it.
330
+ *
331
+ * Named rather than inlined in the rail's `onClick`: inline it sat five
332
+ * braces deep inside the JSX, and it is the same open-then-scroll the
333
+ * submit handler above performs for the first erroring section — so the
334
+ * two now read as the same operation rather than two similar ones.
335
+ *
336
+ * 🛑 Opening is CONDITIONAL on purpose. In `single` mode an unconditional
337
+ * `setOpenIds([id])` would COLLAPSE the section you are already in when you
338
+ * click its own rail entry, so the rail would read as having closed the
339
+ * form.
340
+ */
341
+ const openAndScrollTo = useCallback((id) => {
342
+ if (!openIds.includes(id)) {
343
+ setOpenIds(expandMode === "single" ? [id] : [...openIds, id]);
344
+ }
345
+ scrollToSection(id);
346
+ }, [openIds, setOpenIds, expandMode]);
328
347
  useFormBottomActions({
329
348
  onSubmit: handleSubmit,
330
349
  onCancel,
@@ -391,21 +410,6 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
391
410
  * sections").
392
411
  */
393
412
  const showRail = ordered.length >= 3;
394
- /**
395
- * One rail click: open the section if it is not already, then scroll to it.
396
- * Named rather than inline in the button's `onClick`, where the `if` body
397
- * sat a brace too deep inside the mapped JSX.
398
- *
399
- * Opening is conditional on purpose — in `single` mode an unconditional
400
- * `setOpenIds([id])` would COLLAPSE the section you are already in when you
401
- * click its own rail entry, so the rail would look like it closed the form.
402
- */
403
- const openAndScrollTo = (id) => {
404
- if (!openIds.includes(id)) {
405
- setOpenIds(expandMode === "single" ? [id] : [...openIds, id]);
406
- }
407
- scrollToSection(id);
408
- };
409
413
  /*
410
414
  * `sticky` under the measured header, and it publishes NO height: per the
411
415
  * three-tier rule a sticky element CONSUMES the tier and never joins it.
@@ -444,7 +448,7 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
444
448
  forceOpenIds.includes(section.id);
445
449
  return (_jsxs(Button, { type: "button",
446
450
  /* `ghost`, not `bare` — UI_BUTTON has no `bare` variant. */
447
- variant: "ghost", size: "sm", justify: "between", gap: "sm", className: `w-full text-left ${isOpen ? "appkit-section-rail__item--active" : ""}`, "aria-current": isOpen ? "true" : undefined, onClick: () => openAndScrollTo(section.id), children: [_jsx(Span, { size: "sm", color: isOpen ? "primary" : "muted", children: section.label }), errorCount > 0 && (_jsx(Span, { size: "xs", color: "error", weight: "semibold", children: errorCount }))] }, section.id));
451
+ variant: "ghost", size: "sm", justify: "between", gap: "sm", className: `w-full text-left ${isOpen ? "appkit-section-rail__item--active" : ""}`, "aria-current": isOpen ? "true" : undefined, onClick: () => openAndScrollTo(section.id), children: [_jsx(Span, { size: "sm", color: isOpen ? "primary" : "muted", children: section.label }), submitAttempted && errorCount > 0 && (_jsx(Span, { size: "xs", color: "error", weight: "semibold", children: errorCount }))] }, section.id));
448
452
  }) }) }) })) : null;
449
453
  const panels = (_jsxs(Stack, { gap: "md", children: [ordered.map((section) => {
450
454
  const isRequired = section.required === true;
@@ -288,7 +288,20 @@ function renderField(field, ctx, opts) {
288
288
  * case that most needs searching.
289
289
  */
290
290
  if (options.length > PAGINATED_SELECT_THRESHOLD) {
291
- return (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", weight: "medium", color: "muted", children: [label, required ? " *" : ""] }), _jsx(PaginatedSelect, { value: typeof value === "string" ? value : null, onChange: (v) => set(v ?? ""), options: options, ariaLabel: label, placeholder: `Select ${label.toLowerCase()}…` }), meta.help && _jsx(Text, { size: "xs", color: "muted", children: meta.help }), ctx.errors[name] && (_jsx(Text, { size: "xs", color: "error", role: "alert", children: ctx.errors[name] }))] }, name));
291
+ /*
292
+ * 🛑 The error block here used to be hand-rolled, and was the only
293
+ * field in this generator whose error was not wired like the rest:
294
+ * a `<Text>` with no `id`, nothing pointing `aria-describedby` at it,
295
+ * no `aria-invalid` on the control, and — unlike every `Field*`
296
+ * branch below — the help text was still rendered underneath the
297
+ * error rather than replaced by it.
298
+ *
299
+ * `PaginatedSelect` now takes `name`/`error` and owns all of that,
300
+ * so hand it the error and delete the copy. This is the one branch
301
+ * that every schema-generated form shares, so the fix lands on all
302
+ * of them at once rather than per call site.
303
+ */
304
+ return (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", weight: "medium", color: "muted", children: [label, required ? " *" : ""] }), _jsx(PaginatedSelect, { name: name, error: ctx.errors[name], value: typeof value === "string" ? value : null, onChange: (v) => set(v ?? ""), options: options, ariaLabel: label, placeholder: `Select ${label.toLowerCase()}…` }), meta.help && !ctx.errors[name] && (_jsx(Text, { size: "xs", color: "muted", children: meta.help }))] }, name));
292
305
  }
293
306
  return (_jsx(FieldSelect, { name: name, label: label, hint: meta.help, required: required, value: typeof value === "string" ? value : "", options: options, onChange: set }, name));
294
307
  }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
3
  import React from "react";
4
+ import { z } from "zod";
4
5
  import { useApiMutation } from "@mohasinac/appkit/client";
5
6
  import { Alert, Badge, Button, Div, Heading, Modal, Pagination, Skeleton, Stack, Table, Thead, Tbody, Tr, Th, Td, Text, Textarea, } from "../../../ui/index.js";
6
7
  import { apiClient } from "../../../http/index.js";
@@ -11,6 +12,7 @@ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues.js";
11
12
  import { ValidationError } from "../../../errors/validation-error.js";
12
13
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
13
14
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
15
+ import { QuickEditMenu } from "../../admin/components/QuickEditMenu.js";
14
16
  import { useShipmentItems } from "../hooks/useShipments.js";
15
17
  import { ShipmentItemLinkModal } from "./ShipmentItemLinkModal.js";
16
18
  import { formatCurrency } from "../../../utils/number.formatter.js";
@@ -32,6 +34,30 @@ function parseBulkText(text) {
32
34
  })
33
35
  .filter((row) => row.title.length > 0);
34
36
  }
37
+ /**
38
+ * The three fields a lot item can be corrected on after import.
39
+ *
40
+ * Deliberately NOT the whole of `shipmentItemBaseSchema`: `isForSelfUse`
41
+ * decides whether `price` is required at all and flipping it from a
42
+ * three-field drawer would silently change what the row means to the
43
+ * projections, and `categorySlugs`/`brandSlug` belong to the link flow that
44
+ * already owns them.
45
+ */
46
+ const shipmentItemQuickEditSchema = z.object({
47
+ title: z.string().trim().min(1, "Item title is required"),
48
+ quantity: z.coerce.number().int().min(1, "Quantity must be at least 1."),
49
+ price: z.coerce.number().min(0, "Price cannot be negative."),
50
+ });
51
+ const SHIPMENT_ITEM_QUICK_FIELDS = [
52
+ { name: "title", label: "Title", type: "text", required: true },
53
+ { name: "quantity", label: "Quantity", type: "number", required: true },
54
+ {
55
+ name: "price",
56
+ label: "Projected sale price",
57
+ type: "number",
58
+ helperText: "Feeds this lot's projected revenue, and every profit figure on the projections screen.",
59
+ },
60
+ ];
35
61
  export function AdminShipmentLotItemsView({ shipmentId, lotId }) {
36
62
  const [page, setPage] = React.useState(1);
37
63
  const [pageSize, setPageSize] = React.useState(50);
@@ -113,6 +139,19 @@ export function AdminShipmentLotItemsView({ shipmentId, lotId }) {
113
139
  mutationFn: (itemId) => apiClient.delete(ADMIN_ENDPOINTS.SHIPMENT_LOT_ITEM_BY_ID(shipmentId, lotId, itemId)),
114
140
  onSuccess: () => refetch(),
115
141
  });
142
+ const editItemMutation = useApiMutation({
143
+ successMessage: "Item updated",
144
+ errorMessage: "Could not update that item.",
145
+ mutationFn: ({ itemId, values }) => {
146
+ const parsed = shipmentItemQuickEditSchema.parse(values);
147
+ return apiClient.patch(ADMIN_ENDPOINTS.SHIPMENT_LOT_ITEM_BY_ID(shipmentId, lotId, itemId), parsed);
148
+ },
149
+ onSuccess: () => refetch(),
150
+ });
151
+ /** Named so the row-action array stays a flat list of declarations. */
152
+ const saveItemEdit = React.useCallback(async (itemId, values) => {
153
+ await editItemMutation.mutateAsync({ itemId, values });
154
+ }, [editItemMutation]);
116
155
  const unlinkItemMutation = useApiMutation({
117
156
  successMessage: "Item unlinked",
118
157
  mutationFn: (itemId) => apiClient.patch(ADMIN_ENDPOINTS.SHIPMENT_LOT_ITEM_BY_ID(shipmentId, lotId, itemId), {
@@ -134,7 +173,21 @@ export function AdminShipmentLotItemsView({ shipmentId, lotId }) {
134
173
  return;
135
174
  }
136
175
  addItemMutation.mutate();
137
- }, onValidationChange: () => itemForm.validate(itemDraft), schema: shipmentItemFormSchema, openIds: itemNav.openIds, onOpenChange: itemNav.setOpenIds, submitLabel: "Save Item", cancelLabel: "Cancel", onCancel: () => setShowAddItem(false), isLoading: addItemMutation.isPending })] })), isLoading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" })] })) : items.length === 0 ? (_jsx(Text, { variant: "secondary", children: "No items tracked yet." })) : (_jsxs(Table, { children: [_jsx(Thead, { children: _jsxs(Tr, { children: [_jsx(Th, { children: "Title" }), _jsx(Th, { children: "Qty" }), _jsx(Th, { children: "Price" }), _jsx(Th, { children: "Status" }), _jsx(Th, {})] }) }), _jsx(Tbody, { children: items.map((item) => (_jsxs(Tr, { children: [_jsx(Td, { children: item.title }), _jsx(Td, { children: item.quantity }), _jsx(Td, { children: item.isForSelfUse ? "—" : formatCurrency(item.price ?? 0) }), _jsx(Td, { children: item.isForSelfUse ? (_jsx(Badge, { variant: "secondary", children: "Self use" })) : item.linkedProductId ? (_jsx(Badge, { variant: "success", children: "Linked" })) : (_jsx(Badge, { variant: "default", children: "Unlinked" })) }), _jsxs(Td, { className: "flex gap-[var(--appkit-space-2)]", children: [!item.isForSelfUse &&
176
+ }, onValidationChange: () => itemForm.validate(itemDraft), schema: shipmentItemFormSchema, openIds: itemNav.openIds, onOpenChange: itemNav.setOpenIds, submitLabel: "Save Item", cancelLabel: "Cancel", onCancel: () => setShowAddItem(false), isLoading: addItemMutation.isPending })] })), isLoading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" })] })) : items.length === 0 ? (_jsx(Text, { variant: "secondary", children: "No items tracked yet." })) : (_jsxs(Table, { children: [_jsx(Thead, { children: _jsxs(Tr, { children: [_jsx(Th, { children: "Title" }), _jsx(Th, { children: "Qty" }), _jsx(Th, { children: "Price" }), _jsx(Th, { children: "Status" }), _jsx(Th, {})] }) }), _jsx(Tbody, { children: items.map((item) => (_jsxs(Tr, { children: [_jsx(Td, { children: item.title }), _jsx(Td, { children: item.quantity }), _jsx(Td, { children: item.isForSelfUse ? "—" : formatCurrency(item.price ?? 0) }), _jsx(Td, { children: item.isForSelfUse ? (_jsx(Badge, { variant: "secondary", children: "Self use" })) : item.linkedProductId ? (_jsx(Badge, { variant: "success", children: "Linked" })) : (_jsx(Badge, { variant: "default", children: "Unlinked" })) }), _jsxs(Td, { className: "flex gap-[var(--appkit-space-2)]", children: [_jsx(QuickEditMenu, { actions: [
177
+ {
178
+ label: "Edit item",
179
+ formTitle: `Edit "${item.title}"`,
180
+ schema: shipmentItemQuickEditSchema,
181
+ fields: SHIPMENT_ITEM_QUICK_FIELDS,
182
+ defaultValues: {
183
+ title: item.title,
184
+ quantity: item.quantity,
185
+ price: item.price ?? 0,
186
+ },
187
+ onSubmit: (values) => saveItemEdit(item.id, values),
188
+ submitLabel: "Save item",
189
+ },
190
+ ] }), !item.isForSelfUse &&
138
191
  (item.linkedProductId ? (_jsx(Button, { size: "sm", variant: "ghost", isLoading: unlinkItemMutation.isPending, onClick: () => unlinkItemMutation.mutate(item.id), children: "Unlink" })) : (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setLinkTarget({ itemId: item.id, title: item.title }), children: "Create pre-order link" }))), _jsx(Button, { size: "sm", variant: "ghost", action: ACTIONS.SHIPMENT["delete"], isLoading: deleteItemMutation.isPending, onClick: () => deleteItemMutation.mutate(item.id), children: "Delete" })] })] }, item.id))) })] })), meta && meta.totalPages > 1 && (_jsx(Pagination, { currentPage: page, totalPages: meta.totalPages, onPageChange: setPage, pageSize: pageSize, onPageSizeChange: (size) => {
139
192
  setPageSize(size);
140
193
  setPage(1);
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React from "react";
4
- import { Button, Div, Heading, Modal, Pagination, Select, Skeleton, Stack, Table, Thead, Tbody, Tr, Th, Td, Text } from "../../../ui/index.js";
4
+ import { Button, Div, Heading, Modal, Pagination, Select, Skeleton, Stack, Table, Thead, Tbody, Tr, Th, Td, Text, TextLink } from "../../../ui/index.js";
5
+ import { ROUTES } from "../../../next/routing/route-map.js";
5
6
  import { useShipmentProjections, useShipmentItems } from "../hooks/useShipments.js";
6
7
  import { ShipmentItemLinkModal } from "./ShipmentItemLinkModal.js";
7
8
  import { formatCurrency } from "../../../utils/number.formatter.js";
@@ -25,7 +26,7 @@ export function AdminShipmentProjectionsView() {
25
26
  const [sorts, setSorts] = React.useState(SORT_OPTIONS[0].value);
26
27
  const { lots, meta, isLoading, refetch } = useShipmentProjections({ page, pageSize, sorts });
27
28
  const [pickerLot, setPickerLot] = React.useState(null);
28
- return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { layout: "flex", align: "center", justify: "between", children: [_jsx(Heading, { level: 2, children: "Projections" }), _jsx(Select, { "aria-label": "Sort by", value: sorts, onValueChange: setSorts, options: SORT_OPTIONS })] }), isLoading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" })] })) : lots.length === 0 ? (_jsx(Text, { variant: "secondary", children: "No lots to project yet." })) : (_jsxs(Table, { children: [_jsx(Thead, { children: _jsxs(Tr, { children: [_jsx(Th, { children: "Lot" }), _jsx(Th, { children: "Items" }), _jsx(Th, { children: "Landed Cost" }), _jsx(Th, { children: "Projected Revenue" }), _jsx(Th, { children: "Projected Profit" }), _jsx(Th, {})] }) }), _jsx(Tbody, { children: lots.map((lot) => (_jsxs(Tr, { children: [_jsx(Td, { children: lot.lotName }), _jsx(Td, { children: lot.itemCount }), _jsx(Td, { children: formatCurrency(lot.totalLandedCost) }), _jsx(Td, { children: formatCurrency(lot.projectedRevenue) }), _jsx(Td, { className: lot.projectedProfit < 0 ? "text-error" : "text-success", children: formatCurrency(lot.projectedProfit) }), _jsx(Td, { children: _jsx(Button, { size: "sm", variant: "ghost", onClick: () => setPickerLot(lot), children: "Create pre-order link" }) })] }, lot.id))) })] })), meta && meta.totalPages > 1 && (_jsx(Pagination, { currentPage: page, totalPages: meta.totalPages, onPageChange: setPage, pageSize: pageSize, onPageSizeChange: (size) => {
29
+ return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { layout: "flex", align: "center", justify: "between", children: [_jsx(Heading, { level: 2, children: "Projections" }), _jsx(Select, { "aria-label": "Sort by", value: sorts, onValueChange: setSorts, options: SORT_OPTIONS })] }), isLoading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" })] })) : lots.length === 0 ? (_jsx(Text, { variant: "secondary", children: "No lots to project yet." })) : (_jsxs(Table, { children: [_jsx(Thead, { children: _jsxs(Tr, { children: [_jsx(Th, { children: "Lot" }), _jsx(Th, { children: "Items" }), _jsx(Th, { children: "Landed Cost" }), _jsx(Th, { children: "Projected Revenue" }), _jsx(Th, { children: "Projected Profit" }), _jsx(Th, {})] }) }), _jsx(Tbody, { children: lots.map((lot) => (_jsxs(Tr, { children: [_jsx(Td, { children: _jsx(TextLink, { href: String(ROUTES.ADMIN.SHIPMENT_LOT_ITEMS(lot.shipmentId, lot.id)), children: lot.lotName }) }), _jsx(Td, { children: lot.itemCount }), _jsx(Td, { children: formatCurrency(lot.totalLandedCost) }), _jsx(Td, { children: formatCurrency(lot.projectedRevenue) }), _jsx(Td, { className: lot.projectedProfit < 0 ? "text-error" : "text-success", children: formatCurrency(lot.projectedProfit) }), _jsx(Td, { children: _jsx(Button, { size: "sm", variant: "ghost", onClick: () => setPickerLot(lot), children: "Create pre-order link" }) })] }, lot.id))) })] })), meta && meta.totalPages > 1 && (_jsx(Pagination, { currentPage: page, totalPages: meta.totalPages, onPageChange: setPage, pageSize: pageSize, onPageSizeChange: (size) => {
29
30
  setPageSize(size);
30
31
  setPage(1);
31
32
  } })), pickerLot && (_jsx(LotItemPickerModal, { lot: pickerLot, onClose: () => setPickerLot(null), onLinked: refetch }))] }));
@@ -15,7 +15,7 @@ export declare const createShipmentSchema: z.ZodObject<{
15
15
  laborRatePerHour: z.ZodOptional<z.ZodNumber>;
16
16
  notes: z.ZodOptional<z.ZodString>;
17
17
  }, "strip", z.ZodTypeAny, {
18
- status: "received" | "completed" | "processing" | "cancelled" | "ordered" | "in_transit" | "planning" | "customs";
18
+ status: "received" | "completed" | "processing" | "cancelled" | "in_transit" | "ordered" | "planning" | "customs";
19
19
  shipmentNumber: string;
20
20
  supplierName: string;
21
21
  customsTotal: number;
@@ -29,7 +29,7 @@ export declare const createShipmentSchema: z.ZodObject<{
29
29
  laborHoursSpent?: number | undefined;
30
30
  laborRatePerHour?: number | undefined;
31
31
  }, {
32
- status: "received" | "completed" | "processing" | "cancelled" | "ordered" | "in_transit" | "planning" | "customs";
32
+ status: "received" | "completed" | "processing" | "cancelled" | "in_transit" | "ordered" | "planning" | "customs";
33
33
  shipmentNumber: string;
34
34
  supplierName: string;
35
35
  customsTotal: number;
@@ -58,7 +58,7 @@ export declare const updateShipmentSchema: z.ZodObject<{
58
58
  laborRatePerHour: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
59
59
  notes: z.ZodOptional<z.ZodOptional<z.ZodString>>;
60
60
  }, "strip", z.ZodTypeAny, {
61
- status?: "received" | "completed" | "processing" | "cancelled" | "ordered" | "in_transit" | "planning" | "customs" | undefined;
61
+ status?: "received" | "completed" | "processing" | "cancelled" | "in_transit" | "ordered" | "planning" | "customs" | undefined;
62
62
  trackingNumber?: string | undefined;
63
63
  notes?: string | undefined;
64
64
  carrier?: string | undefined;
@@ -72,7 +72,7 @@ export declare const updateShipmentSchema: z.ZodObject<{
72
72
  laborHoursSpent?: number | undefined;
73
73
  laborRatePerHour?: number | undefined;
74
74
  }, {
75
- status?: "received" | "completed" | "processing" | "cancelled" | "ordered" | "in_transit" | "planning" | "customs" | undefined;
75
+ status?: "received" | "completed" | "processing" | "cancelled" | "in_transit" | "ordered" | "planning" | "customs" | undefined;
76
76
  trackingNumber?: string | undefined;
77
77
  notes?: string | undefined;
78
78
  carrier?: string | undefined;
@@ -0,0 +1,43 @@
1
+ import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
2
+ import { BaseRepository } from "../../../providers/db-firebase";
3
+ import { type ShippingBoxDocument } from "../schemas/firestore";
4
+ /**
5
+ * The shared carton library. See the schema header for why it is global.
6
+ */
7
+ export declare class ShippingBoxesRepository extends BaseRepository<ShippingBoxDocument> {
8
+ static readonly SIEVE_FIELDS: FirebaseSieveFields;
9
+ constructor();
10
+ /**
11
+ * The picker's list. Active only, by name.
12
+ *
13
+ * 🛑 Retired presets are excluded here and NOT deleted, so a draft shipment
14
+ * holding one does not lose it mid-dispatch.
15
+ */
16
+ listActive(model: SieveModel): Promise<FirebaseSieveResult<ShippingBoxDocument>>;
17
+ /**
18
+ * Create a preset, keyed on a slug of its name.
19
+ *
20
+ * 🛑 `box-<slug>` as the document id, so the SAME carton name cannot be added
21
+ * twice by two sellers. That is the whole value of a shared library — two
22
+ * rows called "Standard carton" with different numbers is worse than one
23
+ * row, because the picker gives no way to tell them apart.
24
+ *
25
+ * A colliding create is reported to the caller rather than silently
26
+ * overwriting: the existing row has an author and may be in use, and the
27
+ * second seller's intent ("I want a preset for this") is already satisfied
28
+ * by the row that exists.
29
+ */
30
+ createPreset(input: {
31
+ name: string;
32
+ length: number;
33
+ width: number;
34
+ height: number;
35
+ tareWeight?: number;
36
+ createdByUid: string;
37
+ createdByName: string;
38
+ }): Promise<{
39
+ box: ShippingBoxDocument;
40
+ created: boolean;
41
+ }>;
42
+ }
43
+ export declare const shippingBoxesRepository: ShippingBoxesRepository;
@@ -0,0 +1,90 @@
1
+ import { BaseRepository } from "../../../providers/db-firebase/index.js";
2
+ import { slugify } from "../../../utils/string.formatter.js";
3
+ import { SHIPPING_BOX_COLLECTION, SHIPPING_BOX_FIELDS, SHIPPING_BOX_LIMITS, } from "../schemas/firestore.js";
4
+ /**
5
+ * The shared carton library. See the schema header for why it is global.
6
+ */
7
+ export class ShippingBoxesRepository extends BaseRepository {
8
+ constructor() {
9
+ super(SHIPPING_BOX_COLLECTION);
10
+ }
11
+ /**
12
+ * The picker's list. Active only, by name.
13
+ *
14
+ * 🛑 Retired presets are excluded here and NOT deleted, so a draft shipment
15
+ * holding one does not lose it mid-dispatch.
16
+ */
17
+ async listActive(model) {
18
+ const baseQuery = this.getCollection().where(SHIPPING_BOX_FIELDS.IS_ACTIVE, "==", true);
19
+ return this.sieveQuery(model, ShippingBoxesRepository.SIEVE_FIELDS, {
20
+ baseQuery,
21
+ defaultPageSize: 20,
22
+ maxPageSize: 50,
23
+ });
24
+ }
25
+ /**
26
+ * Create a preset, keyed on a slug of its name.
27
+ *
28
+ * 🛑 `box-<slug>` as the document id, so the SAME carton name cannot be added
29
+ * twice by two sellers. That is the whole value of a shared library — two
30
+ * rows called "Standard carton" with different numbers is worse than one
31
+ * row, because the picker gives no way to tell them apart.
32
+ *
33
+ * A colliding create is reported to the caller rather than silently
34
+ * overwriting: the existing row has an author and may be in use, and the
35
+ * second seller's intent ("I want a preset for this") is already satisfied
36
+ * by the row that exists.
37
+ */
38
+ async createPreset(input) {
39
+ const name = input.name.trim();
40
+ const id = `box-${slugify(name)}`;
41
+ const existing = await this.findById(id);
42
+ if (existing)
43
+ return { box: existing, created: false };
44
+ const now = new Date();
45
+ const box = {
46
+ id,
47
+ name,
48
+ length: clampCm(input.length),
49
+ width: clampCm(input.width),
50
+ height: clampCm(input.height),
51
+ ...(input.tareWeight === undefined ? {} : { tareWeight: clampKg(input.tareWeight) }),
52
+ createdByUid: input.createdByUid,
53
+ createdByName: input.createdByName,
54
+ isActive: true,
55
+ createdAt: now,
56
+ updatedAt: now,
57
+ };
58
+ await this.createWithId(id, box);
59
+ return { box, created: true };
60
+ }
61
+ }
62
+ ShippingBoxesRepository.SIEVE_FIELDS = {
63
+ /*
64
+ * `name` is sortable AND filterable because this list is picked from by
65
+ * typing — `PaginatedSelect` searches it. A field the picker searches that
66
+ * is not allowlisted here would have its clause silently DROPPED and the
67
+ * search would return the whole library while looking like it filtered
68
+ * (Root Cause #62).
69
+ */
70
+ name: { canFilter: true, canSort: true },
71
+ isActive: { canFilter: true, canSort: false },
72
+ createdByUid: { canFilter: true, canSort: false },
73
+ };
74
+ /*
75
+ * Clamped rather than rejected. These bounds exist so a typo cannot reach a
76
+ * courier API as a 4-metre parcel; they are not a tariff, and refusing a
77
+ * legitimate large parcel client-side is worse than letting it through to be
78
+ * priced by `checkServiceability` against the real figures.
79
+ */
80
+ function clampCm(v) {
81
+ if (!Number.isFinite(v))
82
+ return SHIPPING_BOX_LIMITS.minCm;
83
+ return Math.min(SHIPPING_BOX_LIMITS.maxCm, Math.max(SHIPPING_BOX_LIMITS.minCm, v));
84
+ }
85
+ function clampKg(v) {
86
+ if (!Number.isFinite(v))
87
+ return SHIPPING_BOX_LIMITS.minKg;
88
+ return Math.min(SHIPPING_BOX_LIMITS.maxKg, Math.max(SHIPPING_BOX_LIMITS.minKg, v));
89
+ }
90
+ export const shippingBoxesRepository = new ShippingBoxesRepository();