@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
@@ -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,
@@ -408,21 +427,28 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
408
427
  * comment syntax does not parse. That is the fourth time today; the rule is
409
428
  * simply that JSX comments belong in CHILDREN position and nowhere else.
410
429
  */
411
- const rail = showRail ? (_jsx(Div, { overflow: "auto", className: "hidden lg:block w-56 flex-shrink-0 sticky self-start top-[calc(var(--header-height,3.5rem)+1rem)] max-h-[calc(100vh-var(--header-height,3.5rem)-2rem)]", children: _jsx(Nav, { "aria-label": "Form sections", children: _jsx(Stack, { gap: "px", children: ordered.map((section) => {
430
+ const rail = showRail ? (_jsx(Div, { overflow: "auto",
431
+ /*
432
+ * <StickyToolbar> is a horizontal translucent bar: it ships `border-b`, a
433
+ * z-10 layer and the dismiss/re-expand CollapseStrip, and offers no
434
+ * `self-start`, `max-h` or overflow scrolling. Using it here would paint
435
+ * a bottom border under a 14rem sidebar and give it a horizontal bar's
436
+ * collapse affordance.
437
+ *
438
+ * 🛑 The marker below must stay on the line DIRECTLY above the className.
439
+ * Every one of these audits reads `lines[i - 1]` and nothing further, so
440
+ * a tidy explanation with the marker on its first line suppresses
441
+ * nothing while looking suppressed to a human (CLAUDE.md § Rule #5).
442
+ */
443
+ // audit-raw-sticky-toolbar-ok: a vertical RAIL, not a toolbar — see above.
444
+ className: "hidden lg:block w-56 flex-shrink-0 sticky self-start top-[calc(var(--header-height,3.5rem)+1rem)] max-h-[calc(100vh-var(--header-height,3.5rem)-2rem)]", children: _jsx(Nav, { "aria-label": "Form sections", children: _jsx(Stack, { gap: "px", children: ordered.map((section) => {
412
445
  const errorCount = errorCountFor(section);
413
446
  const isOpen = section.required === true ||
414
447
  openIds.includes(section.id) ||
415
448
  forceOpenIds.includes(section.id);
416
449
  return (_jsxs(Button, { type: "button",
417
450
  /* `ghost`, not `bare` — UI_BUTTON has no `bare` variant. */
418
- 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: () => {
419
- if (!openIds.includes(section.id)) {
420
- setOpenIds(expandMode === "single"
421
- ? [section.id]
422
- : [...openIds, section.id]);
423
- }
424
- scrollToSection(section.id);
425
- }, 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));
426
452
  }) }) }) })) : null;
427
453
  const panels = (_jsxs(Stack, { gap: "md", children: [ordered.map((section) => {
428
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 }))] }));
@@ -37,6 +37,7 @@ import { Alert, Button, Card, Form, Heading, Stack, Text } from "../../../ui/ind
37
37
  import { FieldInput } from "../../../ui/forms/FieldInput.js";
38
38
  import { FieldSelect } from "../../../ui/forms/FieldSelect.js";
39
39
  import { TIER_HELP } from "../constants/help-content.js";
40
+ import { toUserMessage } from "../../../errors/error-display-map.js";
40
41
  /** Every tier, unlike the studio — see the header. */
41
42
  const TIERS = ["draft", "standard", "precise", "max"];
42
43
  export function StickerCommitPanel({ file, dimensions, onCommit, onCommitted, }) {
@@ -80,7 +81,7 @@ export function StickerCommitPanel({ file, dimensions, onCommit, onCommitted, })
80
81
  * buyer, which this is not — and the routes already map their own
81
82
  * failures to user-safe text.
82
83
  */
83
- setError(n.message);
84
+ setError(toUserMessage(n.code, undefined, { fallback: "We couldn't commit that sheet. Please try again." }));
84
85
  }
85
86
  finally {
86
87
  setPending(false);
@@ -32,6 +32,7 @@ import { useCallback, useEffect, useState } from "react";
32
32
  import { normalizeError } from "../../../errors/normalize.js";
33
33
  import { Alert, Badge, Button, Card, Heading, Row, Span, Stack, Text, } from "../../../ui/index.js";
34
34
  import { FieldSelect } from "../../../ui/forms/FieldSelect.js";
35
+ import { toUserMessage } from "../../../errors/error-display-map.js";
35
36
  const PROFILES = [
36
37
  { value: "print-shop", label: "Print shop (Roland / Graphtec)" },
37
38
  { value: "cricut-cut", label: "Cricut Joy 2 — cut only" },
@@ -83,7 +84,7 @@ export function StickerPrintPanel({ loadReport, onDownloadCutFile, onDownloadMos
83
84
  * Rendering both the same way would have an operator confirming a DPI to
84
85
  * fix a lapsed session.
85
86
  */
86
- setError(n.message);
87
+ setError(toUserMessage(n.code, undefined, { fallback: "We couldn't prepare that print. Please try again." }));
87
88
  setReport(null);
88
89
  }
89
90
  finally {
@@ -23,11 +23,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
23
23
  * belongs to which stage.
24
24
  *
25
25
  * 🛑 Built on `<SectionForm>`, not a hand-rolled `<Form>`. Three controls is
26
- * where a form stops being trivial, and sectionising is what buys the error
27
- * summary, the jump-to-field link and the pinned mobile action bar for free —
26
+ * where a form stops being trivial, and sectionising is what buys the
27
+ * jump-to-field link and the pinned mobile action bar —
28
28
  * audit-form-sectionised is strict-zero on exactly this, and its grandfather
29
29
  * list is empty and closed.
30
30
  *
31
+ * 🛑 The error summary is NOT free, and this comment used to claim it was.
32
+ * `SectionForm` deliberately does not own a `FormShellContext` — it consumes
33
+ * whichever one the owning view mounts, and tolerates none. So a view that
34
+ * only renders `<SectionForm schema={…}>` gets section badges and no readable
35
+ * message at all. The view must mount the context and render
36
+ * `<FormErrorSummary>` itself, which is what the bottom of this file now does.
37
+ *
31
38
  * EXPORTS: StickerRetryPanel
32
39
  *
33
40
  * @tag domain:sticker-trace
@@ -43,11 +50,14 @@ import { normalizeError } from "../../../errors/normalize.js";
43
50
  import { Alert, Card, Stack, Text } from "../../../ui/index.js";
44
51
  import { FieldInput } from "../../../ui/forms/FieldInput.js";
45
52
  import { FieldSelect } from "../../../ui/forms/FieldSelect.js";
53
+ import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary.js";
54
+ import { FormShellContext, useFormShellState } from "../../../ui/forms/FormShell.js";
46
55
  import { SectionForm } from "../../shell/SectionForm.js";
47
56
  import { TRACE_STAGES } from "../../../_internal/shared/features/sticker-trace/types.js";
48
57
  import { resolveResumeStage, explainResume, } from "../../../_internal/shared/features/sticker-trace/resume.js";
49
58
  import { paramsForTier } from "../../../_internal/shared/features/sticker-trace/config.js";
50
59
  import { TIER_HELP } from "../constants/help-content.js";
60
+ import { toUserMessage } from "../../../errors/error-display-map.js";
51
61
  const TIERS = ["draft", "standard", "precise", "max"];
52
62
  /**
53
63
  * 🛑 `.min(1)` on the name, mirroring `requireName` on the server.
@@ -103,7 +113,7 @@ export function StickerRetryPanel({ source, artifactsPruned = false, onSubmit, o
103
113
  * operator looking at a form that did nothing — and the likeliest causes,
104
114
  * a lapsed session or a rejected name, are both named by the message.
105
115
  */
106
- setError(n.message);
116
+ setError(toUserMessage(n.code, undefined, { fallback: "We couldn't retry that run. Please try again." }));
107
117
  }
108
118
  finally {
109
119
  setPending(false);
@@ -124,5 +134,15 @@ export function StickerRetryPanel({ source, artifactsPruned = false, onSubmit, o
124
134
  render: ({ values: v, onChange: set }) => (_jsxs(Stack, { gap: "comfortable", children: [_jsx(FieldSelect, { name: "requested", label: "Resume from", value: v.requested, options: TRACE_STAGES.map((s) => ({ value: s, label: s })), onChange: (next) => set({ requested: next }) }), notice ? _jsx(Alert, { variant: "warning", children: notice }) : null, artifactsPruned ? (_jsx(Alert, { variant: "warning", children: "This run's artifacts have been cleared, so the retry starts from the beginning whatever you pick here." })) : null, !source.params ? (_jsx(Text, { size: "xs", color: "muted", children: "This run predates parameter recording, so the resume point cannot be checked ahead of time \u2014 the server will decide and tell you." })) : null] })),
125
135
  },
126
136
  ], [source.name, source.params, notice, artifactsPruned]);
127
- return (_jsx(Card, { variant: "default", children: _jsxs(Stack, { gap: "comfortable", children: [error ? _jsx(Alert, { variant: "error", children: error }) : null, _jsx(SectionForm, { sections: sections, values: values, onChange: onChange, onSubmit: submit, schema: retrySchema, submitLabel: "Start retry", isLoading: pending, onCancel: onCancel, cancelLabel: "Cancel" })] }) }));
137
+ /*
138
+ * 🛑 `SectionForm` does NOT provide a FormShellContext — it reads whichever
139
+ * one the owning view mounts, and may legitimately get none. The header of
140
+ * this file says sectionising buys "the error summary … for free", and that
141
+ * was wrong: nothing here mounted a context or rendered a summary, so a
142
+ * schema rejection set the section badge and produced no readable message
143
+ * anywhere. The file passed `audit-form-error-summary` only because a bare
144
+ * `<SectionForm schema=>` was not a recognised call site.
145
+ */
146
+ const { shellCtx } = useFormShellState(retrySchema);
147
+ return (_jsx(Card, { variant: "default", children: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Stack, { gap: "comfortable", children: [error ? _jsx(Alert, { variant: "error", children: error }) : null, _jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: values, onChange: onChange, onSubmit: submit, schema: retrySchema, submitLabel: "Start retry", isLoading: pending, onCancel: onCancel, cancelLabel: "Cancel" })] }) }) }));
128
148
  }
@@ -30,6 +30,7 @@ import { PARAM_HELP, TIER_HELP } from "../constants/help-content.js";
30
30
  import { useLiveTrace } from "../hooks/useLiveTrace.js";
31
31
  import { canvasCodec } from "../../../_internal/client/features/sticker-trace/canvas-codec.js";
32
32
  import { normalizeError } from "../../../errors/normalize.js";
33
+ import { toUserMessage } from "../../../errors/error-display-map.js";
33
34
  /*
34
35
  * Only `draft` is offered. The hook runs on the main thread and draft's
35
36
  * 1200px cap is what keeps one trace near a second; `standard`'s 3000 would
@@ -103,7 +104,7 @@ export function StickerTraceStudio({ renderCommit }) {
103
104
  // the operator is the one who needs to know the commit is blocked.
104
105
  if (live) {
105
106
  setNatural(null);
106
- setProbeError(n.message);
107
+ setProbeError(toUserMessage(n.code, undefined, { fallback: "That probe could not be run." }));
107
108
  }
108
109
  });
109
110
  return () => {
@@ -6,3 +6,5 @@ export { rasterisePartToBox } from "../../_internal/shared/features/sticker-trac
6
6
  export { buildCutFileBytes, buildCutFileForRun } from "../../_internal/server/features/sticker-trace/build-cut-file";
7
7
  export { collectMosaicInputs, type MosaicInputs, type MosaicPartInput, } from "../../_internal/server/features/sticker-trace/mosaic-inputs";
8
8
  export type { PrintPageSizeId } from "../../_internal/shared/features/sticker-trace/export/page-sizes";
9
+ export type { DecodeOptions, ImageCodec, PreviewOptions, ProbeResult, } from "../../_internal/shared/features/sticker-trace/codec";
10
+ export type { DecodedRaster, EncodedImage, Rect as StickerRect, } from "../../_internal/shared/features/sticker-trace/types";
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useQuery, useQueryClient } from "@tanstack/react-query";
4
- import { Button, Card, Details, Div, Row, Stack, Summary, Text } from "../../../ui/index.js";
4
+ import { Button, Card, Details, Div, Row, Stack, Summary, Text, TextLink } from "../../../ui/index.js";
5
+ import { ROUTES } from "../../../next/routing/route-map.js";
5
6
  import { MediaImage } from "../../media/MediaImage.js";
6
7
  import { apiClient } from "../../../http/index.js";
7
8
  import { useApiMutation } from "../../../client/api/useApiMutation.js";
@@ -50,6 +51,6 @@ export function AdminTesterFeedbackIssuesView() {
50
51
  const sortedPhases = Array.from(byPhase.keys()).sort((a, b) => a - b);
51
52
  return (_jsx(Stack, { gap: "md", children: sortedPhases.map((phase) => {
52
53
  const phaseIssues = byPhase.get(phase);
53
- return (_jsxs(Details, { tone: "card", defaultOpen: sortedPhases.length === 1, children: [_jsxs(Summary, { size: "lg", weight: "bold", children: ["Phase ", phase, " (", phaseIssues.length, " issue", phaseIssues.length === 1 ? "" : "s", ")"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "sm", children: phaseIssues.map((issue) => (_jsx(Card, { padding: "md", variant: "default", children: _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { weight: "semibold", children: issue.label }), _jsxs(Text, { size: "sm", color: "muted", children: [issue.groupLabel, " \u203A ", issue.pageLabel] }), _jsxs(Text, { size: "sm", color: "muted", children: ["Reported by ", issue.testerDisplayName] }), issue.comment && _jsx(Text, { size: "sm", children: issue.comment }), issue.screenshotUrl && (_jsx(MediaImage, { src: issue.screenshotUrl, alt: "Tester screenshot", size: "card" })), issue.bugConfirmed ? (_jsxs(Row, { gap: "sm", align: "center", wrap: true, children: [_jsxs(Text, { size: "sm", weight: "semibold", color: "success", children: ["\uD83D\uDC1B Confirmed \u2014 credited to ", issue.bugHunterName ?? "unknown tester"] }), !issue.supersededByItemId && (_jsx(Button, { size: "sm", action: ACTIONS.ADMIN["reopen-checklist-item"], isLoading: reopenMutation.isPending, onClick: () => reopenMutation.mutate(issue.checklistItemId) }))] })) : (_jsx(Div, { children: _jsx(Button, { size: "sm", action: ACTIONS.ADMIN["confirm-bug"], isLoading: confirmBugMutation.isPending, onClick: () => confirmBugMutation.mutate(issue.id) }) }))] }) }, issue.id))) }) })] }, phase));
54
+ return (_jsxs(Details, { tone: "card", defaultOpen: sortedPhases.length === 1, children: [_jsxs(Summary, { size: "lg", weight: "bold", children: ["Phase ", phase, " (", phaseIssues.length, " issue", phaseIssues.length === 1 ? "" : "s", ")"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "sm", children: phaseIssues.map((issue) => (_jsx(Card, { padding: "md", variant: "default", children: _jsxs(Stack, { gap: "xs", children: [_jsx(TextLink, { href: String(ROUTES.ADMIN.TESTER_CHECKLIST_EDIT(issue.checklistItemId)), weight: "semibold", children: issue.label }), _jsxs(Text, { size: "sm", color: "muted", children: [issue.groupLabel, " \u203A ", issue.pageLabel] }), _jsxs(Text, { size: "sm", color: "muted", children: ["Reported by ", issue.testerDisplayName] }), issue.comment && _jsx(Text, { size: "sm", children: issue.comment }), issue.screenshotUrl && (_jsx(MediaImage, { src: issue.screenshotUrl, alt: "Tester screenshot", size: "card" })), issue.bugConfirmed ? (_jsxs(Row, { gap: "sm", align: "center", wrap: true, children: [_jsxs(Text, { size: "sm", weight: "semibold", color: "success", children: ["\uD83D\uDC1B Confirmed \u2014 credited to ", issue.bugHunterName ?? "unknown tester"] }), !issue.supersededByItemId && (_jsx(Button, { size: "sm", action: ACTIONS.ADMIN["reopen-checklist-item"], isLoading: reopenMutation.isPending, onClick: () => reopenMutation.mutate(issue.checklistItemId) }))] })) : (_jsx(Div, { children: _jsx(Button, { size: "sm", action: ACTIONS.ADMIN["confirm-bug"], isLoading: confirmBugMutation.isPending, onClick: () => confirmBugMutation.mutate(issue.id) }) }))] }) }, issue.id))) }) })] }, phase));
54
55
  }) }));
55
56
  }
@@ -2,7 +2,10 @@ import { normalizeError } from "../errors/normalize.js";
2
2
  import { isAccountDisabled } from "../features/auth/role-predicates.js";
3
3
  import { createApiHandlerFactory } from "../next/api/apiHandler.js";
4
4
  import { applyRateLimit } from "../security/index.js";
5
- import { AuthenticationError, AuthorizationError, ERROR_MESSAGES, handleApiError, } from "../errors/index.js";
5
+ import { AuthenticationError, AuthorizationError, ERROR_MESSAGES, } from "../errors/index.js";
6
+ // Next-coupled, so it comes from the defining module — see errors/index.ts.
7
+ // This file is an API-route handler, so `next/server` here is correct.
8
+ import { handleApiError } from "../errors/error-handler.js";
6
9
  import { errorResponse } from "../next/api/api-response.js";
7
10
  import { serverLogger } from "../monitoring/index.js";
8
11
  import { getProviders } from "../contracts/index.js";
package/dist/index.d.ts CHANGED
@@ -52,6 +52,10 @@ export type { FieldSelectProps } from "./ui/index";
52
52
  export { FieldSelect } from "./ui/index";
53
53
  export type { FieldCheckboxProps } from "./ui/index";
54
54
  export { FieldCheckbox } from "./ui/index";
55
+ export type { FieldTextareaProps } from "./ui/index";
56
+ export { FieldTextarea } from "./ui/index";
57
+ export type { FormErrorSummaryProps } from "./ui/index";
58
+ export { FormErrorSummary } from "./ui/index";
55
59
  export { GRID_MAP } from "./ui/index";
56
60
  export { ImageGallery } from "./ui/index";
57
61
  export { ImageLightbox } from "./ui/index";
@@ -321,9 +325,9 @@ export { ERROR_CODES, BID_ERROR_CODES, OFFER_ERROR_CODES } from "./errors/index"
321
325
  export { ERROR_MESSAGES } from "./errors/index";
322
326
  export { NotFoundError } from "./errors/index";
323
327
  export { ValidationError } from "./errors/index";
324
- export { handleApiError } from "./errors/index";
325
- export { isAppError } from "./errors/index";
326
- export { logError } from "./errors/index";
328
+ export { handleApiError } from "./errors/error-handler";
329
+ export { isAppError } from "./errors/error-handler";
330
+ export { logError } from "./errors/error-handler";
327
331
  export type { ErrorCode } from "./errors/index";
328
332
  export { PaymentProviderUnreachableError } from "./errors/index";
329
333
  export { mapToHttpError, HTTP_ERROR_CODES } from "./errors/index";
@@ -2940,7 +2944,7 @@ export type { OwnerQuickActionsProps, OwnerQuickActionsResource } from "./featur
2940
2944
  export type { ListingTypePlugin } from "./_internal/shared/listing-types/_registry";
2941
2945
  export { getListingRule, getCategoryRule, pickOrderType, getSplitKey, runSyncPreflight, CHECKOUT_RULES, CATEGORY_CHECKOUT_RULES, CHECKOUT_MAX_ORDERS_PER_TX, PRIZE_DRAW_MAX_REVEALS_PER_ORDER, BUNDLE_MAX_QTY_PER_TX, STANDARD_MAX_QTY_PER_LINE, } from "./_internal/shared/checkout/rules";
2942
2946
  export type { ListingCheckoutRule, CategoryCheckoutRule, CartItemProductPair, OrderItemInput, RefundPolicy, } from "./_internal/shared/checkout/rules";
2943
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
2947
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, REVIEW_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
2944
2948
  export type { MediaKind, AllowedImageMime, AllowedVideoMime, AllowedDocMime, AllowedMime, } from "./_internal/shared/media/limits";
2945
2949
  export { PRODUCT_FIELDS, PRODUCT_STATUS_TRANSITIONS, ORDER_FIELDS, REVIEW_FIELDS, BID_FIELDS, AD_FIELDS, AD_TRACKED_FIELDS, BLOG_FIELDS, BRAND_FIELDS, WISHLIST_FIELDS, HISTORY_FIELDS, COUPON_USAGE_FIELDS, CAROUSEL_FIELDS, FAQ_FIELDS, HOMEPAGE_SECTION_FIELDS, SITE_SETTINGS_FIELDS, COMMON_FIELDS, OAUTH_STATE_VALUES, SCHEMA_DEFAULTS, } from "./constants/field-names";
2946
2950
  export { SORT_DIR, sortBy } from "./constants/sort";
package/dist/index.js CHANGED
@@ -138,6 +138,8 @@ export { FormGroup } from "./ui/index.js";
138
138
  export { FieldInput } from "./ui/index.js";
139
139
  export { FieldSelect } from "./ui/index.js";
140
140
  export { FieldCheckbox } from "./ui/index.js";
141
+ export { FieldTextarea } from "./ui/index.js";
142
+ export { FormErrorSummary } from "./ui/index.js";
141
143
  // [CLIENT-SSR]-Runs in both SSR and browser â€" React component or hook that does not depend on browser-only APIs.
142
144
  // GRID_MAP - Constant used across modules.
143
145
  export { GRID_MAP } from "./ui/index.js";
@@ -552,13 +554,13 @@ export { NotFoundError } from "./errors/index.js";
552
554
  export { ValidationError } from "./errors/index.js";
553
555
  // [UTIL]-Pure utility â€" no framework or runtime dependency; safe to import from any environment.
554
556
  // handleApiError - Model for handle api error.
555
- export { handleApiError } from "./errors/index.js";
557
+ export { handleApiError } from "./errors/error-handler.js";
556
558
  // [UTIL]-Pure utility â€" no framework or runtime dependency; safe to import from any environment.
557
559
  // isAppError - Model for is app error.
558
- export { isAppError } from "./errors/index.js";
560
+ export { isAppError } from "./errors/error-handler.js";
559
561
  // [UTIL]-Pure utility â€" no framework or runtime dependency; safe to import from any environment.
560
562
  // logError - Helper for log error.
561
- export { logError } from "./errors/index.js";
563
+ export { logError } from "./errors/error-handler.js";
562
564
  // [UTIL]-Pure utility — no framework or runtime dependency; safe to import from any environment.
563
565
  // PaymentProviderUnreachableError - Thrown when the payment gateway API exhausts retries.
564
566
  export { PaymentProviderUnreachableError } from "./errors/index.js";
@@ -5249,7 +5251,7 @@ export { RecordOwnerQuickActions } from "./features/admin/components/RecordOwner
5249
5251
  // Checkout rule registry â€" pure TS, safe in both client and server bundles.
5250
5252
  export { getListingRule, getCategoryRule, pickOrderType, getSplitKey, runSyncPreflight, CHECKOUT_RULES, CATEGORY_CHECKOUT_RULES, CHECKOUT_MAX_ORDERS_PER_TX, PRIZE_DRAW_MAX_REVEALS_PER_ORDER, BUNDLE_MAX_QTY_PER_TX, STANDARD_MAX_QTY_PER_LINE, } from "./_internal/shared/checkout/rules/index.js";
5251
5253
  // Media upload limits â€" pure constants, safe in both client and server bundles.
5252
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits.js";
5254
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, REVIEW_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits.js";
5253
5255
  // ── Field name constants — canonical source of truth for all Firestore field names ──
5254
5256
  // NOTE: STORE_FIELDS, CATEGORY_FIELDS, EVENT_FIELDS, EVENT_ENTRY_FIELDS, PAYOUT_FIELDS,
5255
5257
  // COUPON_FIELDS, CART_FIELDS, USER_FIELDS, ADDRESS_FIELDS, NOTIFICATION_FIELDS,
@@ -182,6 +182,8 @@ export declare const DEFAULT_ROUTE_MAP: {
182
182
  readonly CLAIMED_COUPONS: "/user/coupons";
183
183
  /** Points balance + ledger. 1 point = ₹1, spendable at checkout. */
184
184
  readonly POINTS: "/user/points";
185
+ readonly SESSIONS: "/user/sessions";
186
+ readonly PAYMENT_METHODS: "/user/payment-methods";
185
187
  readonly BECOME_SELLER: "/user/become-seller";
186
188
  readonly CART: "/cart";
187
189
  readonly CHECKOUT: "/checkout";
@@ -299,7 +301,6 @@ export declare const DEFAULT_ROUTE_MAP: {
299
301
  readonly ADMIN: {
300
302
  readonly DASHBOARD: "/admin/dashboard";
301
303
  readonly SHIPMENTS: "/admin/shipments";
302
- readonly INVENTORY: "/admin/inventory";
303
304
  readonly INVENTORY_BOXES: "/admin/inventory/boxes";
304
305
  readonly INVENTORY_BOX_DETAIL: (id: string) => string;
305
306
  readonly INVENTORY_BOX_LABELS: "/admin/inventory/boxes/labels";
@@ -635,6 +636,8 @@ export declare const ROUTES: {
635
636
  readonly CLAIMED_COUPONS: "/user/coupons";
636
637
  /** Points balance + ledger. 1 point = ₹1, spendable at checkout. */
637
638
  readonly POINTS: "/user/points";
639
+ readonly SESSIONS: "/user/sessions";
640
+ readonly PAYMENT_METHODS: "/user/payment-methods";
638
641
  readonly BECOME_SELLER: "/user/become-seller";
639
642
  readonly CART: "/cart";
640
643
  readonly CHECKOUT: "/checkout";
@@ -752,7 +755,6 @@ export declare const ROUTES: {
752
755
  readonly ADMIN: {
753
756
  readonly DASHBOARD: "/admin/dashboard";
754
757
  readonly SHIPMENTS: "/admin/shipments";
755
- readonly INVENTORY: "/admin/inventory";
756
758
  readonly INVENTORY_BOXES: "/admin/inventory/boxes";
757
759
  readonly INVENTORY_BOX_DETAIL: (id: string) => string;
758
760
  readonly INVENTORY_BOX_LABELS: "/admin/inventory/boxes/labels";
@@ -302,6 +302,15 @@ export const DEFAULT_ROUTE_MAP = {
302
302
  CLAIMED_COUPONS: "/user/coupons",
303
303
  /** Points balance + ledger. 1 point = ₹1, spendable at checkout. */
304
304
  POINTS: "/user/points",
305
+ /*
306
+ * 🛑 Both of these had a COMPLETE API and no page. `GET /api/user/sessions`
307
+ * + `DELETE [id]`, and `GET/POST /api/user/payment-methods` + `DELETE
308
+ * [id]`, were all live with zero consumers — and `BuyerAccountGuideView`
309
+ * already told buyers to "view all devices signed in at My Account →
310
+ * Sessions", pointing at a security control that did not exist.
311
+ */
312
+ SESSIONS: "/user/sessions",
313
+ PAYMENT_METHODS: "/user/payment-methods",
305
314
  BECOME_SELLER: "/user/become-seller",
306
315
  CART: "/cart",
307
316
  CHECKOUT: "/checkout",
@@ -433,7 +442,16 @@ export const DEFAULT_ROUTE_MAP = {
433
442
  ADMIN: {
434
443
  DASHBOARD: "/admin/dashboard",
435
444
  SHIPMENTS: "/admin/shipments",
436
- INVENTORY: "/admin/inventory",
445
+ /*
446
+ * 🛑 There is deliberately NO `INVENTORY: "/admin/inventory"` key.
447
+ *
448
+ * I added one and never built the page behind it, and nothing ever
449
+ * referenced it — `audit-dead-route-key` caught it. A ROUTES key is a
450
+ * promise, and the two real surfaces (`INVENTORY_BOXES`,
451
+ * `INVENTORY_MOVEMENTS`) each carry their own nav entry, so an index page
452
+ * would be a third place to list the same two links. Build it only when
453
+ * there is a third child worth grouping.
454
+ */
437
455
  INVENTORY_BOXES: "/admin/inventory/boxes",
438
456
  INVENTORY_BOX_DETAIL: (id) => `/admin/inventory/boxes/${id}`,
439
457
  /*