@mohasinac/appkit 4.60.1 → 4.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) 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/auctions/actions.d.ts +1 -1
  6. package/dist/_internal/server/features/auctions/actions.js +12 -1
  7. package/dist/_internal/server/features/blog/actions.d.ts +1 -1
  8. package/dist/_internal/server/features/blog/actions.js +12 -1
  9. package/dist/_internal/server/features/brands/actions.d.ts +1 -1
  10. package/dist/_internal/server/features/brands/actions.js +12 -1
  11. package/dist/_internal/server/features/cart/actions.d.ts +1 -1
  12. package/dist/_internal/server/features/cart/actions.js +12 -1
  13. package/dist/_internal/server/features/checkout/actions.js +0 -1
  14. package/dist/_internal/server/features/events/actions.d.ts +1 -1
  15. package/dist/_internal/server/features/events/actions.js +12 -1
  16. package/dist/_internal/server/features/gateway-events/redact.js +14 -2
  17. package/dist/_internal/server/features/history/actions.d.ts +1 -1
  18. package/dist/_internal/server/features/history/actions.js +12 -1
  19. package/dist/_internal/server/features/lottery/actions.d.ts +1 -1
  20. package/dist/_internal/server/features/lottery/actions.js +12 -1
  21. package/dist/_internal/server/features/orders/actions.d.ts +1 -1
  22. package/dist/_internal/server/features/orders/actions.js +12 -1
  23. package/dist/_internal/server/features/orders/cancellation-refund.js +0 -1
  24. package/dist/_internal/server/features/orders/service.js +31 -1
  25. package/dist/_internal/server/features/payouts/actions.d.ts +1 -1
  26. package/dist/_internal/server/features/payouts/actions.js +12 -1
  27. package/dist/_internal/server/features/points/award.js +9 -1
  28. package/dist/_internal/server/features/pre-orders/actions.d.ts +1 -1
  29. package/dist/_internal/server/features/pre-orders/actions.js +12 -1
  30. package/dist/_internal/server/features/prize-draws/actions.d.ts +1 -1
  31. package/dist/_internal/server/features/prize-draws/actions.js +12 -1
  32. package/dist/_internal/server/features/products/actions.d.ts +1 -1
  33. package/dist/_internal/server/features/products/actions.js +12 -1
  34. package/dist/_internal/server/features/promotions/actions.d.ts +1 -1
  35. package/dist/_internal/server/features/promotions/actions.js +12 -1
  36. package/dist/_internal/server/features/protection-claims/actions.d.ts +2 -2
  37. package/dist/_internal/server/features/protection-claims/actions.js +21 -12
  38. package/dist/_internal/server/features/raffle/actions.d.ts +1 -1
  39. package/dist/_internal/server/features/raffle/actions.js +12 -1
  40. package/dist/_internal/server/features/refunds/actions.d.ts +1 -3
  41. package/dist/_internal/server/features/refunds/actions.js +14 -2
  42. package/dist/_internal/server/features/reviews/actions.d.ts +1 -1
  43. package/dist/_internal/server/features/reviews/actions.js +12 -1
  44. package/dist/_internal/server/features/wishlist/actions.d.ts +1 -1
  45. package/dist/_internal/server/features/wishlist/actions.js +12 -1
  46. package/dist/_internal/server/jobs/core/obligationSweep.js +0 -1
  47. package/dist/_internal/server/jobs/core/paymentReconcile.js +93 -76
  48. package/dist/_internal/server/jobs/core/testDataCleanup.d.ts +1 -1
  49. package/dist/_internal/server/jobs/core/testDataCleanup.js +22 -1
  50. package/dist/_internal/server/jobs/handlers/onShipmentAllocationSync.js +16 -4
  51. package/dist/_internal/shared/actions/action-registry.js +149 -9
  52. package/dist/_internal/shared/features/promotions/schema.d.ts +66 -0
  53. package/dist/_internal/shared/features/promotions/schema.js +7 -0
  54. package/dist/_internal/shared/media/limits.d.ts +10 -0
  55. package/dist/_internal/shared/media/limits.js +10 -0
  56. package/dist/_internal/shared/obligations/deadlines.js +11 -1
  57. package/dist/_internal/shared/tester/window.d.ts +25 -0
  58. package/dist/_internal/shared/tester/window.js +66 -0
  59. package/dist/_internal/shared/types/action-result.js +15 -1
  60. package/dist/client/api/surface-error.js +16 -2
  61. package/dist/client/api/useApiMutation.js +17 -1
  62. package/dist/client.d.ts +1 -1
  63. package/dist/client.js +1 -1
  64. package/dist/constants/api-endpoints.d.ts +3 -0
  65. package/dist/constants/api-endpoints.js +1 -0
  66. package/dist/constants/field-names.d.ts +17 -0
  67. package/dist/constants/field-names.js +23 -0
  68. package/dist/core/server-action.js +19 -3
  69. package/dist/core/unit-of-work.js +15 -1
  70. package/dist/errors/error-mapping.js +26 -1
  71. package/dist/errors/firestore-codes.d.ts +37 -0
  72. package/dist/errors/firestore-codes.js +52 -0
  73. package/dist/errors/index.d.ts +0 -1
  74. package/dist/errors/index.js +12 -2
  75. package/dist/features/account/components/UserOffersPanel.js +5 -3
  76. package/dist/features/admin/components/AdminActionIndexView.js +100 -5
  77. package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
  78. package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
  79. package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
  80. package/dist/features/admin/components/AdminCarouselView.js +96 -4
  81. package/dist/features/admin/components/AdminCouponEditorView.js +8 -0
  82. package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
  83. package/dist/features/admin/components/AdminGatewayEventsView.js +2 -2
  84. package/dist/features/admin/components/AdminMediaView.js +36 -3
  85. package/dist/features/admin/components/AdminNavigationView.js +4 -1
  86. package/dist/features/admin/components/AdminOffersView.js +2 -1
  87. package/dist/features/admin/components/AdminPaymentClustersView.js +19 -2
  88. package/dist/features/admin/components/AdminProductEditorView.js +2 -2
  89. package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
  90. package/dist/features/admin/components/AdminSectionsView.js +486 -224
  91. package/dist/features/admin/components/DataListingView.js +2 -0
  92. package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
  93. package/dist/features/admin/components/analytics/AdminStatCard.js +4 -1
  94. package/dist/features/admin/hooks/useOptimisticRowFlag.d.ts +33 -0
  95. package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
  96. package/dist/features/admin/repository/notification.repository.d.ts +15 -0
  97. package/dist/features/admin/repository/notification.repository.js +32 -0
  98. package/dist/features/admin/repository/site-settings.repository.d.ts +2 -1
  99. package/dist/features/admin/repository/site-settings.repository.js +7 -0
  100. package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
  101. package/dist/features/admin/schemas/action-index-form.js +115 -0
  102. package/dist/features/auctions/components/MarketplaceAuctionCard.js +1 -1
  103. package/dist/features/auth/components/RegisterForm.js +2 -1
  104. package/dist/features/catalogue/actions/approve-catalogue-listing.d.ts +1 -1
  105. package/dist/features/catalogue/actions/approve-catalogue-listing.js +12 -1
  106. package/dist/features/catalogue/actions/list-from-catalogue.d.ts +1 -1
  107. package/dist/features/catalogue/actions/list-from-catalogue.js +12 -1
  108. package/dist/features/catalogue/actions/submit-for-approval.d.ts +1 -1
  109. package/dist/features/catalogue/actions/submit-for-approval.js +12 -1
  110. package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
  111. package/dist/features/categories/repository/price-observation.repository.d.ts +19 -0
  112. package/dist/features/categories/repository/price-observation.repository.js +21 -2
  113. package/dist/features/events/actions/event-actions.js +4 -1
  114. package/dist/features/events/components/AdminEventEditorView.js +79 -50
  115. package/dist/features/events/components/EventsListPageView.js +2 -1
  116. package/dist/features/events/schemas/firestore.d.ts +17 -0
  117. package/dist/features/events/schemas/firestore.js +1 -0
  118. package/dist/features/events/utils/sort-pinned.d.ts +33 -0
  119. package/dist/features/events/utils/sort-pinned.js +46 -0
  120. package/dist/features/gateway-events/repository/gateway-events.repository.js +3 -40
  121. package/dist/features/gateway-events/schemas/index.d.ts +2 -2
  122. package/dist/features/inventory/components/AdminStorageBoxesView.js +55 -39
  123. package/dist/features/inventory/components/ReceiveStationView.js +61 -17
  124. package/dist/features/inventory/repository/storage-boxes.repository.d.ts +3 -1
  125. package/dist/features/inventory/repository/storage-boxes.repository.js +36 -3
  126. package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
  127. package/dist/features/obligations/repository/obligations.repository.js +10 -1
  128. package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
  129. package/dist/features/orders/components/RefundRequestView.js +4 -4
  130. package/dist/features/orders/schemas/firestore.d.ts +19 -0
  131. package/dist/features/orders/schemas/firestore.js +19 -0
  132. package/dist/features/orders/schemas/index.d.ts +6 -6
  133. package/dist/features/payment-cases/repository/payment-cases.repository.d.ts +1 -0
  134. package/dist/features/payment-cases/repository/payment-cases.repository.js +3 -14
  135. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
  136. package/dist/features/products/components/GroupMemberPicker.js +4 -1
  137. package/dist/features/products/components/MarketplaceBundleCard.js +1 -1
  138. package/dist/features/products/components/MarketplacePrizeDrawCard.js +1 -1
  139. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
  140. package/dist/features/products/components/ProductDetailActions.js +2 -1
  141. package/dist/features/products/components/ProductForm.js +2 -2
  142. package/dist/features/products/components/ProductGrid.js +1 -1
  143. package/dist/features/promotions/repository/claimed-coupons.repository.js +31 -1
  144. package/dist/features/promotions/schemas/firestore.d.ts +22 -0
  145. package/dist/features/promotions/schemas/firestore.js +22 -0
  146. package/dist/features/promotions/schemas/index.d.ts +479 -6
  147. package/dist/features/promotions/schemas/index.js +153 -0
  148. package/dist/features/protection-claims/repository/protection-claims.repository.js +22 -15
  149. package/dist/features/seller/components/FulfillmentView.js +3 -2
  150. package/dist/features/seller/components/SellerAddressesView.js +32 -4
  151. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
  152. package/dist/features/seller/components/SellerBundlesView.js +53 -14
  153. package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
  154. package/dist/features/seller/components/SellerListingTypeView.js +56 -10
  155. package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
  156. package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
  157. package/dist/features/seller/components/SellerProductsView.js +5 -1
  158. package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
  159. package/dist/features/seller/components/SellerShippingView.js +2 -1
  160. package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
  161. package/dist/features/seller/components/SellerSupportView.js +69 -2
  162. package/dist/features/seller/schemas/coupon-form.d.ts +5 -0
  163. package/dist/features/seller/schemas/coupon-form.js +18 -0
  164. package/dist/features/shell/SectionForm.js +35 -9
  165. package/dist/features/shell/build-sections.js +14 -1
  166. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
  167. package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
  168. package/dist/features/sticker-trace/components/StickerCommitPanel.js +2 -1
  169. package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
  170. package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
  171. package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
  172. package/dist/features/sticker-trace/server.d.ts +2 -0
  173. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
  174. package/dist/http/api-handler.js +4 -1
  175. package/dist/index.d.ts +8 -4
  176. package/dist/index.js +6 -4
  177. package/dist/next/routing/route-map.d.ts +4 -2
  178. package/dist/next/routing/route-map.js +19 -1
  179. package/dist/schemas/registry.d.ts +15 -10
  180. package/dist/schemas/sieve.d.ts +3 -3
  181. package/dist/server.d.ts +1 -2
  182. package/dist/server.js +6 -4
  183. package/dist/styles.css +51 -0
  184. package/dist/ui/components/Checkbox.js +10 -2
  185. package/dist/ui/components/DateInput.js +8 -1
  186. package/dist/ui/components/Input.js +22 -2
  187. package/dist/ui/components/PaginatedSelect.d.ts +8 -19
  188. package/dist/ui/components/PaginatedSelect.js +0 -0
  189. package/dist/ui/components/Radio.js +8 -1
  190. package/dist/ui/components/Select.js +10 -2
  191. package/dist/ui/components/TagInput.d.ts +8 -1
  192. package/dist/ui/components/TagInput.js +4 -3
  193. package/dist/ui/components/Textarea.js +10 -2
  194. package/dist/ui/forms/FieldCheckbox.js +1 -1
  195. package/dist/ui/forms/FormErrorList.d.ts +0 -16
  196. package/dist/ui/forms/FormErrorList.js +34 -1
  197. package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
  198. package/dist/ui/forms/apply-zod-issues.js +23 -1
  199. package/package.json +1 -1
  200. package/scripts/seed-cli.mjs +1822 -1812
  201. package/dist/features/tester/utils/phases.d.ts +0 -48
  202. package/dist/features/tester/utils/phases.js +0 -139
@@ -37,6 +37,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
37
37
  */
38
38
  import React, { useCallback, useState } from "react";
39
39
  import { Badge, ConfirmDeleteModal, RowActionMenu, Span, Text } from "../../../ui/index.js";
40
+ import { apiClient } from "../../../http/index.js";
40
41
  import { useEntityDelete } from "../../../react/hooks/useEntityDelete.js";
41
42
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
42
43
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
@@ -46,6 +47,7 @@ import { PRODUCT_FIELDS } from "../../../constants/field-names.js";
46
47
  import { SIEVE_OP, sieveFilter } from "../../../utils/sieve-builder.js";
47
48
  import { pluginFor } from "../../../_internal/shared/listing-types/_registry.js";
48
49
  import { toCurrency, toRecordArray, toRelativeDate, toStringValue, } from "../../admin/hooks/useAdminListingData.js";
50
+ import { useOptimisticRowFlag } from "../../admin/hooks/useOptimisticRowFlag.js";
49
51
  import { DataListingView } from "../../admin/components/DataListingView.js";
50
52
  import { ROW_ACTION_META, SELLER_BULK_ACTIONS } from "../../products/constants/action-defs.js";
51
53
  import { useAvailabilityScope } from "../../products/hooks/useAvailabilityScope.js";
@@ -204,6 +206,17 @@ export function SellerListingTypeView({ type, onCreateClick, onEditClick, onDele
204
206
  await performDelete(id);
205
207
  setDeleteTargetId(null);
206
208
  }, [performDelete]);
209
+ /*
210
+ * Optimistic status, via the shared hook — see its header for why a
211
+ * rollback must DELETE the override key rather than write back a captured
212
+ * value, and why the key is cleared on success too.
213
+ */
214
+ const statusFlag = useOptimisticRowFlag({
215
+ mutate: (id, next) => apiClient.patch(SELLER_ENDPOINTS.PRODUCT_BY_ID(id), { status: next }),
216
+ invalidate: [["seller", plugin.tabSlug]],
217
+ successMessage: (next) => next === "published" ? "Listing published." : "Listing unpublished.",
218
+ errorMessage: "Could not change that listing's status.",
219
+ });
207
220
  const handleEdit = useCallback((id) => {
208
221
  if (onEditClick)
209
222
  onEditClick(id);
@@ -233,7 +246,15 @@ export function SellerListingTypeView({ type, onCreateClick, onEditClick, onDele
233
246
  {
234
247
  key: "status",
235
248
  header: "Status",
236
- render: (row) => (_jsx(Badge, { variant: row.status === "active" ? "active" : "inactive", size: "xs", className: "capitalize", children: row.status })),
249
+ /*
250
+ * Reads the optimistic override so the badge and the row action agree
251
+ * the instant one is used — otherwise the menu would say "Unpublish"
252
+ * while the badge beside it still said published, until the refetch.
253
+ */
254
+ render: (row) => {
255
+ const status = statusFlag.resolve(row.id, row.status);
256
+ return (_jsx(Badge, { variant: status === "active" || status === "published" ? "active" : "inactive", size: "xs", className: "capitalize", children: status }));
257
+ },
237
258
  },
238
259
  textCell("createdAt", spec.createdHeader ?? "Created"),
239
260
  ];
@@ -291,15 +312,40 @@ export function SellerListingTypeView({ type, onCreateClick, onEditClick, onDele
291
312
  },
292
313
  }))
293
314
  : undefined,
294
- renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
295
- { label: ACTIONS.STORE["edit-listing"].label, onClick: () => handleEdit(row.id) },
296
- {
297
- label: ACTIONS.STORE["delete-listing"].label,
298
- destructive: true,
299
- onClick: () => setDeleteTargetId(row.id),
300
- disabled: deletingId === row.id,
301
- },
302
- ] })),
315
+ renderRowActions: (row) => {
316
+ const isPublished = statusFlag.resolve(row.id, row.status) === "published";
317
+ return (_jsx(RowActionMenu, { actions: [
318
+ { label: ACTIONS.STORE["edit-listing"].label, onClick: () => handleEdit(row.id) },
319
+ /*
320
+ * 🛑 This one entry lights up SIX seller pages — art, classified,
321
+ * digital codes, live, spares and stickers are each a ~25-line
322
+ * wrapper around this component.
323
+ *
324
+ * `status` was a read-only Badge on all of them, so changing one
325
+ * listing from published to draft meant opening the full editor,
326
+ * even though `PATCH /api/store/products/[id]` has always taken
327
+ * `status` and SellerProductsView already drives that exact call.
328
+ *
329
+ * `ACTIONS.STORE["publish-listing"]` / `["unpublish-listing"]`
330
+ * were defined with ZERO call sites — this is what finally wires
331
+ * them, rather than reinventing the labels inline.
332
+ */
333
+ {
334
+ label: isPublished
335
+ ? ACTIONS.STORE["unpublish-listing"].label
336
+ : ACTIONS.STORE["publish-listing"].label,
337
+ separator: true,
338
+ disabled: statusFlag.pendingId === row.id,
339
+ onClick: () => { void statusFlag.set(row.id, isPublished ? "draft" : "published"); },
340
+ },
341
+ {
342
+ label: ACTIONS.STORE["delete-listing"].label,
343
+ destructive: true,
344
+ onClick: () => setDeleteTargetId(row.id),
345
+ disabled: deletingId === row.id,
346
+ },
347
+ ] }));
348
+ },
303
349
  };
304
350
  return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), deleteTargetId && (_jsx(ConfirmDeleteModal, { isOpen: true, title: `Delete ${spec.deleteLabel}`, message: `Are you sure you want to delete this ${spec.deleteLabel.toLowerCase()}? This cannot be undone.`, onConfirm: () => handleDelete(deleteTargetId), onClose: () => setDeleteTargetId(null), isDeleting: deletingId === deleteTargetId }))] }));
305
351
  }
@@ -8,6 +8,8 @@ import { Badge, ConfirmDeleteModal, Div, Row, RowActionMenu, Span, Text, useToas
8
8
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
9
9
  import { ROUTES } from "../../../constants/index.js";
10
10
  import { toRecordArray, toStringValue } from "../hooks/useSellerListingData.js";
11
+ import { useOptimisticRowFlag } from "../../admin/hooks/useOptimisticRowFlag.js";
12
+ import { apiClient } from "../../../http/index.js";
11
13
  import { DataListingView } from "../../admin/components/DataListingView.js";
12
14
  const CLS_TYPE_PILL = "inline-flex items-center rounded-full px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-semibold uppercase tracking-wide bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-300";
13
15
  const DEFAULT_SORT = "-createdAt";
@@ -42,6 +44,22 @@ const COLUMNS = [
42
44
  },
43
45
  ];
44
46
  export function SellerPayoutMethodsView({ onCreateClick, onEditClick, onDelete, onSetDefault, }) {
47
+ /*
48
+ * 🛑 `isActive` was a READ-ONLY badge here, and the update schema was built
49
+ * to take it on its own — shipping-config's even says so in a comment: "a
50
+ * PATCH may legitimately carry only `isActive`". Nothing in the client had
51
+ * ever sent one, so flipping one boolean meant a full-editor round trip.
52
+ *
53
+ * The optimistic/rollback logic lives in the hook because eight listings
54
+ * grew this control at once; see its header for why rollback must DELETE
55
+ * the override key rather than write back a captured value.
56
+ */
57
+ const activeFlag = useOptimisticRowFlag({
58
+ mutate: (id, next) => apiClient.patch(SELLER_ENDPOINTS.PAYOUT_METHOD_BY_ID(id), { isActive: next }),
59
+ invalidate: [["seller", "payout-methods", "listing"]],
60
+ successMessage: (next) => (next ? "Payout method activated." : "Payout method deactivated."),
61
+ errorMessage: "Could not change that payout method's status.",
62
+ });
45
63
  const [deleteTargetId, setDeleteTargetId] = useState(null);
46
64
  const { deletingId, handleDelete: performDelete } = useEntityDelete({
47
65
  endpoint: SELLER_ENDPOINTS.PAYOUT_METHOD_BY_ID,
@@ -114,13 +132,27 @@ export function SellerPayoutMethodsView({ onCreateClick, onEditClick, onDelete,
114
132
  label: String(item.label ?? ""),
115
133
  type: String(item.type ?? ""),
116
134
  isDefault: Boolean(item.isDefault),
117
- isActive: Boolean(item.isActive),
135
+ // Resolved through the pending override so the Status badge and the
136
+ // row toggle cannot disagree between the click and the refetch.
137
+ isActive: activeFlag.resolve(toStringValue(item.id, `pm-${index}`), Boolean(item.isActive)),
118
138
  maskedIdentifier: getMaskedIdentifier(item),
119
139
  })),
120
140
  getTotal: (response, mappedRows) => (typeof response.total === "number" ? response.total : mappedRows.length),
121
141
  buildFilters: () => undefined,
122
142
  primaryAction: { label: "New Method", onClick: handleCreate },
123
143
  renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
144
+ /*
145
+ * 🛑 `isActive` was a read-only badge and the update schema was
146
+ * built to accept it alone. A menu entry rather than a switch here,
147
+ * to match the other actions in this menu — the menu is already
148
+ * click-stopped in all three renderers.
149
+ */
150
+ {
151
+ label: row.isActive ? "Deactivate" : "Activate",
152
+ separator: true,
153
+ disabled: activeFlag.pendingId === row.id,
154
+ onClick: () => { void activeFlag.set(row.id, !row.isActive); },
155
+ },
124
156
  ...(!row.isDefault
125
157
  ? [{ label: "Set as Default", disabled: settingDefaultId === row.id, onClick: () => handleSetDefault(row.id) }]
126
158
  : []),
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useMemo, useState } from "react";
4
4
  import { z } from "zod";
5
5
  import { Alert, Badge, Checkbox, Div, FormField, FormGroup, Heading, Label, Row, Span, Stack, Text, Toggle } from "../../../ui/index.js";
@@ -8,6 +8,7 @@ import { FormShellContext, useFormShellState, applyZodIssues, FormErrorSummary }
8
8
  import { SectionForm, useSectionFormNav, annotate } from "../../shell/index.js";
9
9
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
10
10
  import { normalizeError } from "../../../errors/normalize.js";
11
+ import { toUserMessage } from "../../../errors/error-display-map.js";
11
12
  const __P = {
12
13
  p3: "p-[var(--appkit-space-3)]",
13
14
  };
@@ -153,6 +154,20 @@ const DEFAULT_DRAFT = {
153
154
  */
154
155
  advancedListingTypesEnabled: false,
155
156
  };
157
+ /**
158
+ * One radio group for a single-value field, with ONE error message.
159
+ *
160
+ * Extracted because the payout-method and account-type groups were the same
161
+ * eleven lines twice, and because adding the error handling to both tipped
162
+ * this view past `audit-code-quality`'s component-size threshold. The error
163
+ * placement is the part worth sharing: `aria-invalid` goes on every radio so
164
+ * assistive tech marks the whole group, but the message renders ONCE
165
+ * underneath — the field is one value, so an `error` per radio would print
166
+ * and announce the same sentence for every option.
167
+ */
168
+ function RadioFieldGroup({ name, value, options, error, disabled, onSelect, renderOption, layout: Layout, }) {
169
+ return (_jsxs(_Fragment, { children: [_jsx(Layout, { gap: "sm", children: options.map((option) => (_jsxs(Label, { layout: "flex", gap: "md", className: "cursor-pointer", children: [_jsx(Checkbox, { bare: true, type: "radio", name: name, value: option, checked: value === option, onChange: () => onSelect(option), className: "accent-[var(--appkit-color-primary)]", disabled: disabled, "aria-invalid": error ? "true" : undefined }), renderOption(option)] }, option))) }), error && (_jsx(Text, { size: "sm", variant: "error", role: "alert", children: error }))] }));
170
+ }
156
171
  export function SellerPayoutSettingsView({ apiBase = SELLER_ENDPOINTS.PAYOUT_SETTINGS }) {
157
172
  const [draft, setDraft] = useState(DEFAULT_DRAFT);
158
173
  const [current, setCurrent] = useState(null);
@@ -253,7 +268,22 @@ export function SellerPayoutSettingsView({ apiBase = SELLER_ENDPOINTS.PAYOUT_SET
253
268
  if (Array.isArray(json?.issues) && json.issues.length > 0) {
254
269
  applyZodIssues(json.issues, setFieldError);
255
270
  }
256
- setError(json?.error ?? "Failed to save");
271
+ /*
272
+ * `json.error` is the SERVER's sentence — never shown to a user
273
+ * (Rule #9.6). Resolve the stable `code` instead; `toUserMessage`
274
+ * terminates in this fallback and can never return server text.
275
+ *
276
+ * Only a banner when the issues above attached nothing: if they did,
277
+ * the fields now carry the detail and a banner repeating a vaguer
278
+ * version of it is noise.
279
+ */
280
+ const attached = Array.isArray(json?.issues) &&
281
+ json.issues.some((i) => i?.path?.length);
282
+ if (!attached) {
283
+ setError(toUserMessage(json?.code, undefined, {
284
+ fallback: "We couldn't save your payout settings.",
285
+ }));
286
+ }
257
287
  return;
258
288
  }
259
289
  clearErrors();
@@ -263,8 +293,17 @@ export function SellerPayoutSettingsView({ apiBase = SELLER_ENDPOINTS.PAYOUT_SET
263
293
  setDraft((prev) => ({ ...prev, accountNumber: "" }));
264
294
  }
265
295
  catch (err) {
266
- void normalizeError(err);
267
- setError(err.message);
296
+ /*
297
+ * `normalizeError` was already being called and its result DISCARDED,
298
+ * then the raw `.message` shown anyway — a thrown value's message is
299
+ * written for a developer, and this is the path a network failure or a
300
+ * bundler fault takes (Root Cause #86 rendered a require stack this
301
+ * way). Use the normalized code; the sentence is ours.
302
+ */
303
+ const normalized = normalizeError(err);
304
+ setError(toUserMessage(normalized.code, undefined, {
305
+ fallback: "We couldn't save your payout settings. Please try again.",
306
+ }));
268
307
  }
269
308
  finally {
270
309
  setSaving(false);
@@ -283,12 +322,12 @@ export function SellerPayoutSettingsView({ apiBase = SELLER_ENDPOINTS.PAYOUT_SET
283
322
  required: true,
284
323
  quick: true,
285
324
  fields: ["method", "upiId", "accountHolderName", "accountNumber", "ifscCode", "bankName", "accountType"],
286
- render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Payout Method" }), current && (_jsx(Row, { className: "mb-2", align: "center", gap: "sm", children: _jsx(Badge, { variant: current.isConfigured ? "success" : "warning", children: current.isConfigured ? "Payout configured" : "Not configured" }) })), current?.isConfigured && (_jsx(Alert, { variant: "info", children: current.method === "upi"
325
+ render: ({ values, onChange, errors }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Payout Method" }), current && (_jsx(Row, { className: "mb-2", align: "center", gap: "sm", children: _jsx(Badge, { variant: current.isConfigured ? "success" : "warning", children: current.isConfigured ? "Payout configured" : "Not configured" }) })), current?.isConfigured && (_jsx(Alert, { variant: "info", children: current.method === "upi"
287
326
  ? `Current UPI: ${current.upiId}`
288
- : `Current bank: ${current.bankAccount?.bankName ?? ""} — ••••${current.bankAccount?.accountNumberMasked?.slice(-4) ?? "••••"} (${current.bankAccount?.accountType})` })), _jsx(Stack, { gap: "sm", children: ([
289
- { value: "upi", label: "UPI", desc: "Instant payouts via UPI VPA (e.g. name@upi)." },
290
- { value: "bank_transfer", label: "Bank Transfer", desc: "NEFT/RTGS to your bank account within 2–3 business days." },
291
- ]).map(({ value, label, desc }) => (_jsxs("label", { className: "flex items-center gap-[var(--appkit-space-3)] p-[var(--appkit-space-3)] rounded-lg border border-[var(--appkit-color-border)] cursor-pointer has-[:checked]:border-[var(--appkit-color-primary)] has-[:checked]:bg-[var(--appkit-color-primary)]/5", children: [_jsx(Checkbox, { bare: true, type: "radio", name: "payoutMethod", value: value, checked: values.method === value, onChange: () => onChange({ method: value }), className: "accent-[var(--appkit-color-primary)]", disabled: busy }), _jsxs(Div, { children: [_jsx(Text, { weight: "medium", children: label }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: desc })] })] }, value))) }), values.method === "upi" && (_jsx(FormField, { name: "upiId", label: "UPI ID (VPA)", type: "text", value: values.upiId, onChange: (v) => onChange({ upiId: v }), placeholder: "yourname@upi", helpText: "Ensure this VPA is registered and active.", disabled: busy })), values.method === "bank_transfer" && (_jsxs(Stack, { gap: "md", className: "mt-2", children: [_jsx(FormField, { name: "accountHolderName", label: "Account Holder Name", type: "text", value: values.accountHolderName, onChange: (v) => onChange({ accountHolderName: v }), placeholder: "Name as on bank account", disabled: busy }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "accountNumber", label: "Account Number", type: "text", value: values.accountNumber, onChange: (v) => onChange({ accountNumber: v }), placeholder: "Enter full account number", helpText: current?.bankAccount ? `Saved: ••••${current.bankAccount.accountNumberMasked?.slice(-4) ?? "••••"}` : "Stored securely, never displayed in full.", disabled: busy }), _jsx(FormField, { name: "ifscCode", label: "IFSC Code", type: "text", value: values.ifscCode, onChange: (v) => onChange({ ifscCode: v.toUpperCase() }), placeholder: "e.g. SBIN0001234", disabled: busy })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "bankName", label: "Bank Name", type: "text", value: values.bankName, onChange: (v) => onChange({ bankName: v }), placeholder: "e.g. State Bank of India", disabled: busy }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-1.5", size: "sm", weight: "medium", children: "Account Type" }), _jsx(Row, { gap: "xs", children: ["savings", "current"].map((t) => (_jsxs(Label, { layout: "flex", gap: "md", className: "cursor-pointer", children: [_jsx(Checkbox, { bare: true, type: "radio", name: "accountType", value: t, checked: values.accountType === t, onChange: () => onChange({ accountType: t }), className: "accent-[var(--appkit-color-primary)]", disabled: busy }), _jsx(Text, { size: "sm", transform: "capitalize", children: t })] }, t))) })] })] })] }))] })),
327
+ : `Current bank: ${current.bankAccount?.bankName ?? ""} — ••••${current.bankAccount?.accountNumberMasked?.slice(-4) ?? "••••"} (${current.bankAccount?.accountType})` })), _jsxs(Stack, { gap: "sm", children: [([
328
+ { value: "upi", label: "UPI", desc: "Instant payouts via UPI VPA (e.g. name@upi)." },
329
+ { value: "bank_transfer", label: "Bank Transfer", desc: "NEFT/RTGS to your bank account within 2–3 business days." },
330
+ ]).map(({ value, label, desc }) => (_jsxs("label", { className: "flex items-center gap-[var(--appkit-space-3)] p-[var(--appkit-space-3)] rounded-lg border border-[var(--appkit-color-border)] cursor-pointer has-[:checked]:border-[var(--appkit-color-primary)] has-[:checked]:bg-[var(--appkit-color-primary)]/5", children: [_jsx(Checkbox, { bare: true, type: "radio", name: "payoutMethod", value: value, checked: values.method === value, onChange: () => onChange({ method: value }), className: "accent-[var(--appkit-color-primary)]", disabled: busy, "aria-invalid": errors.method ? "true" : undefined }), _jsxs(Div, { children: [_jsx(Text, { weight: "medium", children: label }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: desc })] })] }, value))), errors.method && (_jsx(Text, { size: "sm", variant: "error", role: "alert", children: errors.method }))] }), values.method === "upi" && (_jsx(FormField, { name: "upiId", label: "UPI ID (VPA)", type: "text", value: values.upiId, onChange: (v) => onChange({ upiId: v }), placeholder: "yourname@upi", helpText: "Ensure this VPA is registered and active.", disabled: busy })), values.method === "bank_transfer" && (_jsxs(Stack, { gap: "md", className: "mt-2", children: [_jsx(FormField, { name: "accountHolderName", label: "Account Holder Name", type: "text", value: values.accountHolderName, onChange: (v) => onChange({ accountHolderName: v }), placeholder: "Name as on bank account", disabled: busy }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "accountNumber", label: "Account Number", type: "text", value: values.accountNumber, onChange: (v) => onChange({ accountNumber: v }), placeholder: "Enter full account number", helpText: current?.bankAccount ? `Saved: ••••${current.bankAccount.accountNumberMasked?.slice(-4) ?? "••••"}` : "Stored securely, never displayed in full.", disabled: busy }), _jsx(FormField, { name: "ifscCode", label: "IFSC Code", type: "text", value: values.ifscCode, onChange: (v) => onChange({ ifscCode: v.toUpperCase() }), placeholder: "e.g. SBIN0001234", disabled: busy })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "bankName", label: "Bank Name", type: "text", value: values.bankName, onChange: (v) => onChange({ bankName: v }), placeholder: "e.g. State Bank of India", disabled: busy }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-1.5", size: "sm", weight: "medium", children: "Account Type" }), _jsx(RadioFieldGroup, { name: "accountType", value: values.accountType, options: ["savings", "current"], error: errors.accountType, disabled: busy, onSelect: (t) => onChange({ accountType: t }), layout: Row, renderOption: (t) => (_jsx(Text, { size: "sm", transform: "capitalize", children: t })) })] })] })] }))] })),
292
331
  },
293
332
  {
294
333
  id: "tax",
@@ -641,7 +641,11 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
641
641
  : `/products/${row.id}`,
642
642
  renderRowActions: (row) => {
643
643
  const isPublished = (statusOverrides.get(row.id) ?? row.status) === "published";
644
- return (_jsxs(Row, { gap: "xs", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); handleEdit(row); }, "aria-label": ACTIONS.STORE["edit-listing"].ariaLabel, children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void openAssembly(row, "assemble"); }, "aria-label": ACTIONS.STORE["assemble-from-parts"].ariaLabel, title: ACTIONS.STORE["assemble-from-parts"].label, disabled: assembleLoading, children: _jsx(Wrench, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void openAssembly(row, "part-out"); }, "aria-label": ACTIONS.STORE["part-out-listing"].ariaLabel, title: ACTIONS.STORE["part-out-listing"].label, disabled: assembleLoading, children: _jsx(Scissors, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void handleTogglePublish(row); }, "aria-label": isPublished ? "Unpublish" : "Publish", title: isPublished ? "Unpublish" : "Publish", disabled: publishingId === row.id, children: isPublished ? _jsx(EyeOff, { className: "w-4 h-4" }) : _jsx(Eye, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void handleDuplicate(row); }, "aria-label": "Duplicate listing", title: "Duplicate", children: "\u29C9" }), onDeleteProduct && (_jsx(Button, { variant: "ghost", size: "sm", action: ACTIONS.STORE["delete-listing"], onClick: (e) => { e.stopPropagation(); void handleDelete(row); }, disabled: deletingId === row.id, className: "text-[var(--appkit-color-error)] hover:bg-[var(--appkit-color-border-subtle)]", children: _jsx(Trash2, { className: "w-4 h-4" }) }))] }));
644
+ return (_jsxs(Row, { gap: "xs", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); handleEdit(row); }, "aria-label": ACTIONS.STORE["edit-listing"].ariaLabel, children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void openAssembly(row, "assemble"); }, "aria-label": ACTIONS.STORE["assemble-from-parts"].ariaLabel, title: ACTIONS.STORE["assemble-from-parts"].label, disabled: assembleLoading, children: _jsx(Wrench, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void openAssembly(row, "part-out"); }, "aria-label": ACTIONS.STORE["part-out-listing"].ariaLabel, title: ACTIONS.STORE["part-out-listing"].label, disabled: assembleLoading, children: _jsx(Scissors, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void handleTogglePublish(row); }, "aria-label": isPublished
645
+ ? ACTIONS.STORE["unpublish-listing"].label
646
+ : ACTIONS.STORE["publish-listing"].label, title: isPublished
647
+ ? ACTIONS.STORE["unpublish-listing"].description
648
+ : ACTIONS.STORE["publish-listing"].description, disabled: publishingId === row.id, children: isPublished ? _jsx(EyeOff, { className: "w-4 h-4" }) : _jsx(Eye, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void handleDuplicate(row); }, "aria-label": "Duplicate listing", title: "Duplicate", children: "\u29C9" }), onDeleteProduct && (_jsx(Button, { variant: "ghost", size: "sm", action: ACTIONS.STORE["delete-listing"], onClick: (e) => { e.stopPropagation(); void handleDelete(row); }, disabled: deletingId === row.id, className: "text-[var(--appkit-color-error)] hover:bg-[var(--appkit-color-border-subtle)]", children: _jsx(Trash2, { className: "w-4 h-4" }) }))] }));
645
649
  },
646
650
  };
647
651
  return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), setLocationOpen && (_jsx(PhysicalLocationModal, { count: selectedIdsForLocation.length, onSave: (loc) => handleSetLocation(loc, selectedIdsForLocation), onClose: () => setSetLocationOpen(false) })), setStockOpen && (_jsx(BulkStockModal, { count: selectedIdsForStock.length, busy: stockBusy, onSave: (qty) => handleSetStock(qty, selectedIdsForStock), onClose: () => setSetStockOpen(false) })), assembleFor && assembleData && (_jsx(AssemblyDrawer, { isOpen: true,
@@ -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
  }
@@ -199,6 +199,7 @@ export declare const adminCouponFormSchema: z.ZodEffects<z.ZodObject<{
199
199
  description: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
200
200
  buyQuantity: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
201
201
  getQuantity: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
202
+ maxItemPrice: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
202
203
  currentUsage: z.ZodOptional<z.ZodNumber>;
203
204
  tiers: z.ZodOptional<z.ZodArray<z.ZodObject<{
204
205
  minAmount: z.ZodNumber;
@@ -233,6 +234,7 @@ export declare const adminCouponFormSchema: z.ZodEffects<z.ZodObject<{
233
234
  discountValue: number;
234
235
  }[] | undefined;
235
236
  currentUsage?: number | undefined;
237
+ maxItemPrice?: string | undefined;
236
238
  maxDiscount?: string | undefined;
237
239
  buyQuantity?: string | undefined;
238
240
  getQuantity?: string | undefined;
@@ -257,6 +259,7 @@ export declare const adminCouponFormSchema: z.ZodEffects<z.ZodObject<{
257
259
  discountValue: number;
258
260
  }[] | undefined;
259
261
  currentUsage?: number | undefined;
262
+ maxItemPrice?: string | undefined;
260
263
  maxDiscount?: string | undefined;
261
264
  buyQuantity?: string | undefined;
262
265
  getQuantity?: string | undefined;
@@ -281,6 +284,7 @@ export declare const adminCouponFormSchema: z.ZodEffects<z.ZodObject<{
281
284
  discountValue: number;
282
285
  }[] | undefined;
283
286
  currentUsage?: number | undefined;
287
+ maxItemPrice?: string | undefined;
284
288
  maxDiscount?: string | undefined;
285
289
  buyQuantity?: string | undefined;
286
290
  getQuantity?: string | undefined;
@@ -305,6 +309,7 @@ export declare const adminCouponFormSchema: z.ZodEffects<z.ZodObject<{
305
309
  discountValue: number;
306
310
  }[] | undefined;
307
311
  currentUsage?: number | undefined;
312
+ maxItemPrice?: string | undefined;
308
313
  maxDiscount?: string | undefined;
309
314
  buyQuantity?: string | undefined;
310
315
  getQuantity?: string | undefined;
@@ -236,6 +236,24 @@ export const adminCouponFormSchema = couponFormBase
236
236
  section: "basics", order: 7, row: "pair", kind: "number",
237
237
  when: (v) => v.type === "buy_x_get_y",
238
238
  }),
239
+ /*
240
+ * The per-ITEM price ceiling — "buy 3 get 1 free on items under ₹2,000".
241
+ *
242
+ * 🛑 Genuinely optional, unlike the buy/get pair: `superRefine` below does
243
+ * NOT demand it when the type is buy_x_get_y, because a BOGO with no
244
+ * ceiling is a perfectly ordinary offer and every one written before
245
+ * 2026-10-22 is exactly that. Leaving it blank means no ceiling.
246
+ *
247
+ * 🛑 Not to be confused with `minPurchase`, which is the ORDER total. This
248
+ * is one unit's price, so a line of three ₹1,500 tops qualifies on all
249
+ * three units rather than failing a ₹4,500 test.
250
+ */
251
+ maxItemPrice: annotate(numericString("Maximum item price").optional(), {
252
+ section: "basics", order: 8, row: "pair", kind: "number",
253
+ when: (v) => v.type === "buy_x_get_y",
254
+ label: "Max item price (₹)",
255
+ help: "Only items at or below this price qualify. Leave blank for no limit.",
256
+ }),
239
257
  /*
240
258
  * Written by orders, never by this form. `t3-mirror` renders it read-only
241
259
  * instead of as a box an admin could set to whatever they liked — which is