@mohasinac/appkit 4.14.0 → 4.16.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 (206) hide show
  1. package/dist/_internal/server/features/auctions/actions.js +2 -2
  2. package/dist/_internal/server/features/cart/service.d.ts +3 -3
  3. package/dist/_internal/server/features/checkout/actions.js +19 -0
  4. package/dist/_internal/server/features/checkout/source-context.d.ts +5 -0
  5. package/dist/_internal/server/features/checkout/source-context.js +157 -0
  6. package/dist/_internal/server/features/offers/adapters.d.ts +103 -0
  7. package/dist/_internal/server/features/offers/adapters.js +88 -0
  8. package/dist/_internal/server/features/offers/index.d.ts +2 -0
  9. package/dist/_internal/server/features/offers/index.js +1 -0
  10. package/dist/_internal/server/features/orders/adapters.js +24 -0
  11. package/dist/_internal/server/jobs/core/offerExpiry.js +10 -10
  12. package/dist/_internal/shared/actions/action-registry.d.ts +9 -2
  13. package/dist/_internal/shared/actions/action-registry.js +458 -7
  14. package/dist/_internal/shared/features/auctions/schema.d.ts +14 -2
  15. package/dist/_internal/shared/features/auctions/schema.js +13 -1
  16. package/dist/_internal/shared/features/blog/schema.d.ts +4 -4
  17. package/dist/_internal/shared/features/cart/schema.d.ts +25 -20
  18. package/dist/_internal/shared/features/cart/schema.js +15 -0
  19. package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +22 -22
  20. package/dist/_internal/shared/features/orders/schema.d.ts +4 -4
  21. package/dist/_internal/shared/features/products/schema.d.ts +27 -27
  22. package/dist/_internal/shared/features/promotions/schema.d.ts +4 -4
  23. package/dist/_internal/shared/features/reviews/schema.d.ts +2 -2
  24. package/dist/_internal/shared/history/history.d.ts +22 -0
  25. package/dist/_internal/shared/history/history.js +41 -1
  26. package/dist/client.d.ts +13 -2
  27. package/dist/client.js +20 -2
  28. package/dist/constants/field-names.d.ts +8 -6
  29. package/dist/constants/field-names.js +48 -6
  30. package/dist/contracts/extend.d.ts +29 -0
  31. package/dist/contracts/extend.js +20 -0
  32. package/dist/contracts/index.d.ts +2 -1
  33. package/dist/contracts/index.js +2 -0
  34. package/dist/features/account/components/UserBidsView.js +12 -4
  35. package/dist/features/account/components/UserReturnsView.d.ts +3 -0
  36. package/dist/features/account/components/UserReturnsView.js +17 -1
  37. package/dist/features/admin/components/AdminAuditLogView.js +1 -0
  38. package/dist/features/admin/components/AdminBidsView.js +15 -2
  39. package/dist/features/admin/components/AdminBlogEditorView.js +65 -33
  40. package/dist/features/admin/components/AdminFeatureEditorView.js +35 -6
  41. package/dist/features/admin/components/AdminNavEditorView.js +15 -5
  42. package/dist/features/admin/components/AdminNewsletterView.js +18 -2
  43. package/dist/features/admin/components/AdminOffersView.js +62 -18
  44. package/dist/features/admin/components/AdminPaymentMethodsView.js +24 -4
  45. package/dist/features/admin/components/AdminScammerEditorView.d.ts +2 -1
  46. package/dist/features/admin/components/AdminScammerEditorView.js +3 -13
  47. package/dist/features/admin/components/AdminScammersView.js +3 -8
  48. package/dist/features/admin/components/AdminSectionsView.js +32 -23
  49. package/dist/features/admin/components/AdminSessionsView.js +17 -2
  50. package/dist/features/admin/components/AdminViewCards.js +24 -17
  51. package/dist/features/admin/components/DataListingView.js +15 -1
  52. package/dist/features/admin/components/DataTable.js +7 -17
  53. package/dist/features/admin/components/ViewAuditLogEntryModal.js +1 -0
  54. package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +4 -2
  55. package/dist/features/admin/components/sections/adminSectionsTypes.js +45 -27
  56. package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
  57. package/dist/features/admin/constants/filter-tabs.js +1 -0
  58. package/dist/features/admin/schemas/homepage-section-form.d.ts +22 -0
  59. package/dist/features/admin/schemas/homepage-section-form.js +68 -0
  60. package/dist/features/admin/schemas/nav-item-form.d.ts +74 -0
  61. package/dist/features/admin/schemas/nav-item-form.js +72 -0
  62. package/dist/features/auctions/components/AuctionBidsTable.d.ts +10 -1
  63. package/dist/features/auctions/components/AuctionBidsTable.js +22 -4
  64. package/dist/features/auctions/components/bid-detail-fields.d.ts +29 -0
  65. package/dist/features/auctions/components/bid-detail-fields.js +103 -0
  66. package/dist/features/auctions/schemas/index.d.ts +14 -14
  67. package/dist/features/audit-log/schemas/firestore.d.ts +14 -0
  68. package/dist/features/audit-log/schemas/firestore.js +7 -0
  69. package/dist/features/auth/schemas/index.d.ts +2 -2
  70. package/dist/features/blog/actions/blog-actions.d.ts +7 -7
  71. package/dist/features/blog/schemas/blog-form.d.ts +80 -0
  72. package/dist/features/blog/schemas/blog-form.js +100 -0
  73. package/dist/features/blog/schemas/index.d.ts +10 -10
  74. package/dist/features/cart/schemas/index.d.ts +3 -3
  75. package/dist/features/catalogue/schemas/validation.d.ts +12 -12
  76. package/dist/features/categories/schemas/index.d.ts +16 -16
  77. package/dist/features/collections/schemas/index.d.ts +8 -8
  78. package/dist/features/events/components/AdminEventEditorView.js +32 -54
  79. package/dist/features/events/components/AdminEventEntriesView.js +22 -3
  80. package/dist/features/events/schemas/event-form.d.ts +113 -0
  81. package/dist/features/events/schemas/event-form.js +82 -0
  82. package/dist/features/events/schemas/index.d.ts +2 -2
  83. package/dist/features/faq/actions/faq-actions.d.ts +6 -6
  84. package/dist/features/faq/schemas/index.d.ts +6 -6
  85. package/dist/features/grouped/schemas/grouped-listing-form.d.ts +101 -0
  86. package/dist/features/grouped/schemas/grouped-listing-form.js +89 -0
  87. package/dist/features/grouped/schemas/index.d.ts +4 -4
  88. package/dist/features/homepage/actions/homepage-section-actions.d.ts +11 -3
  89. package/dist/features/homepage/actions/homepage-section-actions.js +33 -1
  90. package/dist/features/homepage/schemas/index.d.ts +71 -40
  91. package/dist/features/homepage/schemas/index.js +24 -0
  92. package/dist/features/messages/schemas/index.d.ts +2 -2
  93. package/dist/features/orders/components/OrderStatusTimeline.d.ts +5 -1
  94. package/dist/features/orders/components/OrderStatusTimeline.js +39 -20
  95. package/dist/features/orders/repository/orders.repository.js +5 -0
  96. package/dist/features/orders/schemas/firestore.d.ts +36 -1
  97. package/dist/features/orders/schemas/index.d.ts +24 -24
  98. package/dist/features/orders/types/index.d.ts +35 -7
  99. package/dist/features/pre-orders/schemas/index.d.ts +4 -4
  100. package/dist/features/products/components/GroupSettingsPanel.js +5 -1
  101. package/dist/features/products/schemas/index.d.ts +38 -38
  102. package/dist/features/products/schemas/product-features.validators.d.ts +75 -2
  103. package/dist/features/products/schemas/product-features.validators.js +63 -0
  104. package/dist/features/promotions/schemas/index.d.ts +14 -14
  105. package/dist/features/reviews/schemas/index.d.ts +14 -14
  106. package/dist/features/scams/constants/scammer-status-display.d.ts +28 -0
  107. package/dist/features/scams/constants/scammer-status-display.js +79 -0
  108. package/dist/features/scams/schemas/index.d.ts +2 -2
  109. package/dist/features/search/schemas/index.d.ts +2 -2
  110. package/dist/features/seller/actions/offer-actions.js +32 -2
  111. package/dist/features/seller/components/OfferPhaseTimeline.d.ts +59 -0
  112. package/dist/features/seller/components/OfferPhaseTimeline.js +107 -0
  113. package/dist/features/seller/components/SellerBidsView.js +11 -4
  114. package/dist/features/seller/components/SellerCouponEditorView.js +17 -17
  115. package/dist/features/seller/components/SellerOffersView.js +2 -1
  116. package/dist/features/seller/components/SellerPayoutSettingsView.js +38 -14
  117. package/dist/features/seller/repository/offer.repository.d.ts +69 -8
  118. package/dist/features/seller/repository/offer.repository.js +127 -23
  119. package/dist/features/seller/schemas/coupon-form.d.ts +72 -0
  120. package/dist/features/seller/schemas/coupon-form.js +116 -0
  121. package/dist/features/seller/schemas/firestore.d.ts +56 -2
  122. package/dist/features/seller/schemas/firestore.js +27 -0
  123. package/dist/features/seller/schemas/index.d.ts +2 -2
  124. package/dist/features/seller/schemas/offer-forms.d.ts +21 -0
  125. package/dist/features/seller/schemas/offer-forms.js +21 -0
  126. package/dist/features/shell/build-sections.d.ts +63 -0
  127. package/dist/features/shell/build-sections.js +207 -0
  128. package/dist/features/shell/field-ui-meta.d.ts +115 -0
  129. package/dist/features/shell/field-ui-meta.js +324 -0
  130. package/dist/features/shell/index.d.ts +4 -0
  131. package/dist/features/shell/index.js +2 -0
  132. package/dist/features/shipments/schemas/validation.d.ts +22 -22
  133. package/dist/features/status-history/components/StatusTimeline.d.ts +74 -0
  134. package/dist/features/status-history/components/StatusTimeline.js +46 -0
  135. package/dist/features/status-history/index.d.ts +11 -0
  136. package/dist/features/status-history/index.js +10 -0
  137. package/dist/features/store-extensions/components/ReviewDecisionModal.d.ts +27 -0
  138. package/dist/features/store-extensions/components/ReviewDecisionModal.js +82 -0
  139. package/dist/features/store-extensions/index.d.ts +12 -0
  140. package/dist/features/store-extensions/index.js +12 -0
  141. package/dist/features/store-extensions/schemas/admin-notification-form.d.ts +36 -0
  142. package/dist/features/store-extensions/schemas/admin-notification-form.js +114 -0
  143. package/dist/features/store-extensions/schemas/analytics-forms.d.ts +57 -0
  144. package/dist/features/store-extensions/schemas/analytics-forms.js +158 -0
  145. package/dist/features/store-extensions/schemas/custom-role-form.d.ts +85 -0
  146. package/dist/features/store-extensions/schemas/custom-role-form.js +118 -0
  147. package/dist/features/store-extensions/schemas/firestore.d.ts +21 -1
  148. package/dist/features/store-extensions/schemas/firestore.js +60 -0
  149. package/dist/features/store-extensions/schemas/google-config-form.d.ts +53 -0
  150. package/dist/features/store-extensions/schemas/google-config-form.js +105 -0
  151. package/dist/features/store-extensions/schemas/item-request-create-form.d.ts +28 -0
  152. package/dist/features/store-extensions/schemas/item-request-create-form.js +80 -0
  153. package/dist/features/store-extensions/schemas/listing-template-form.d.ts +78 -0
  154. package/dist/features/store-extensions/schemas/listing-template-form.js +87 -0
  155. package/dist/features/store-extensions/schemas/moderation-review-form.d.ts +49 -0
  156. package/dist/features/store-extensions/schemas/moderation-review-form.js +101 -0
  157. package/dist/features/store-extensions/schemas/payout-method-form.d.ts +153 -0
  158. package/dist/features/store-extensions/schemas/payout-method-form.js +155 -0
  159. package/dist/features/store-extensions/schemas/report-create-form.d.ts +25 -0
  160. package/dist/features/store-extensions/schemas/report-create-form.js +79 -0
  161. package/dist/features/store-extensions/schemas/report-review-form.d.ts +47 -0
  162. package/dist/features/store-extensions/schemas/report-review-form.js +111 -0
  163. package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +148 -0
  164. package/dist/features/store-extensions/schemas/shipping-config-form.js +104 -0
  165. package/dist/features/store-extensions/schemas/store-category-form.d.ts +54 -0
  166. package/dist/features/store-extensions/schemas/store-category-form.js +73 -0
  167. package/dist/features/stores/schemas/index.d.ts +4 -4
  168. package/dist/features/support/schemas/index.d.ts +4 -4
  169. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +350 -0
  170. package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
  171. package/dist/index.d.ts +12 -2
  172. package/dist/index.js +12 -2
  173. package/dist/next/routing/route-map.d.ts +16 -2
  174. package/dist/next/routing/route-map.js +9 -2
  175. package/dist/schemas/firestore-value.d.ts +8 -0
  176. package/dist/schemas/firestore-value.js +44 -0
  177. package/dist/schemas/index.d.ts +1 -1
  178. package/dist/schemas/index.js +1 -1
  179. package/dist/schemas/registry.d.ts +734 -101
  180. package/dist/schemas/registry.js +64 -1
  181. package/dist/seed/bids-seed-data.js +30 -0
  182. package/dist/seed/offers-seed-data.js +212 -7
  183. package/dist/seed/orders-seed-data.js +336 -1
  184. package/dist/seed/products-auctions-seed-data.js +41 -0
  185. package/dist/server.d.ts +2 -0
  186. package/dist/server.js +5 -0
  187. package/dist/styles.css +1 -1
  188. package/dist/tailwind-utilities.css +1 -1
  189. package/dist/ui/DataTable.js +4 -2
  190. package/dist/ui/columns/build-columns.d.ts +6 -1
  191. package/dist/ui/columns/build-columns.js +53 -0
  192. package/dist/ui/columns/cell-renderers.d.ts +68 -0
  193. package/dist/ui/columns/cell-renderers.js +111 -0
  194. package/dist/ui/columns/column-renderers.d.ts +6 -1
  195. package/dist/ui/columns/column-renderers.js +6 -1
  196. package/dist/ui/columns/index.d.ts +2 -1
  197. package/dist/ui/columns/index.js +5 -1
  198. package/dist/ui/components/Button.js +27 -2
  199. package/dist/ui/components/RecordDetailModal.d.ts +10 -1
  200. package/dist/ui/components/RecordDetailModal.js +2 -2
  201. package/dist/ui/icons/icon-registry.d.ts +65 -0
  202. package/dist/ui/icons/icon-registry.js +115 -0
  203. package/package.json +2 -1
  204. package/scripts/node-esm-loader.mjs +65 -0
  205. package/scripts/seed-cli-loader.mjs +6 -49
  206. package/scripts/seed-cli.mjs +1 -1
@@ -0,0 +1,79 @@
1
+ /*
2
+ * WHY: "How does a scammer status look and read" had FOUR hand-written answers
3
+ * and they had already diverged in a way that shipped a real defect:
4
+ *
5
+ * · `AdminScammersView.STATUS_BADGE` — Record<string, string>
6
+ * · `AdminScammerEditorView.STATUS_COLOR` — Record<string, string>
7
+ * · `AdminScammerEditorView.STATUS_OPTIONS` — a literal label list
8
+ * · `ADMIN_SCAMMER_STATUS_TABS` — the filter chips
9
+ *
10
+ * Both colour maps carried the SAME broken `removed` entry:
11
+ *
12
+ * "bg-[…surface] text-[…muted] bg-[…surface-elevated] text-[…muted]"
13
+ *
14
+ * — two backgrounds and two inks on one element. Tailwind emits all four
15
+ * and the winner is decided by stylesheet order, not by position in the
16
+ * class string, so which background actually painted was not what either
17
+ * map said. One bug, two copies: the Duplication Framework's bug-fix
18
+ * multiplier, which is what makes this a consolidate rather than a
19
+ * third copy.
20
+ *
21
+ * WHAT: One display module beside the union it describes.
22
+ *
23
+ * ## Everything here is keyed `Record<ScammerStatus, …>`
24
+ *
25
+ * Not `Record<string, …>`. A status added to the union without a label or a
26
+ * badge is then a COMPILE error rather than a row that silently renders with
27
+ * the fallback styling of a status it is not — which is precisely how the
28
+ * loose maps hid the broken `removed` entry.
29
+ *
30
+ * EXPORTS:
31
+ * SCAMMER_STATUS_LABEL, SCAMMER_STATUS_BADGE, SCAMMER_STATUS_OPTIONS,
32
+ * toScammerStatus
33
+ *
34
+ * @tag domain:scams
35
+ * @tag layer:constants
36
+ * @tag pattern:none
37
+ * @tag access:isomorphic
38
+ * @tag consumers:AdminScammersView,AdminScammerEditorView
39
+ * @tag sideEffects:none
40
+ */
41
+ import { ScammerStatusValues } from "../schemas/firestore";
42
+ /** Human-readable label. Replaces two `.replace(/_/g, " ")` call sites, which
43
+ * rendered "pending review" — mechanically correct, and not a written label. */
44
+ export const SCAMMER_STATUS_LABEL = {
45
+ pending_review: "Pending review",
46
+ verified: "Verified",
47
+ rejected: "Rejected",
48
+ removed: "Removed",
49
+ };
50
+ /**
51
+ * Inline-chip styling. Every entry is a `bg-{status}-surface` + `text-{status}`
52
+ * pair per Recurrent Root Cause #67 — the two tokens invert together with the
53
+ * theme, which a literal `text-white` would not.
54
+ *
55
+ * `removed` is the neutral case: it has no status colour because it is not a
56
+ * status outcome, it is a withdrawal. ONE background, ONE ink.
57
+ */
58
+ export const SCAMMER_STATUS_BADGE = {
59
+ pending_review: "bg-warning-surface text-warning",
60
+ verified: "bg-success-surface text-success",
61
+ rejected: "bg-error-surface text-error",
62
+ removed: "bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]",
63
+ };
64
+ /** Dropdown options, derived from the union so one cannot go missing. */
65
+ export const SCAMMER_STATUS_OPTIONS = Object.values(ScammerStatusValues).map((value) => ({ value, label: SCAMMER_STATUS_LABEL[value] }));
66
+ /**
67
+ * Narrow an unknown API value to a real status.
68
+ *
69
+ * The list view used `toStringValue(item.status, "pending_review")`, which
70
+ * produces a `string` — so an unrecognised value flowed on as if it were a
71
+ * status and every consumer fell back to the pending styling. This maps
72
+ * anything unrecognised to `pending_review` explicitly, and the return type
73
+ * says so.
74
+ */
75
+ export function toScammerStatus(value) {
76
+ return Object.values(ScammerStatusValues).includes(value)
77
+ ? value
78
+ : ScammerStatusValues.PENDING_REVIEW;
79
+ }
@@ -87,9 +87,9 @@ export declare const scammerFirestoreSchema: z.ZodObject<{
87
87
  updatedAt: string | Date | z.objectOutputType<{
88
88
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
89
89
  }, z.ZodTypeAny, "passthrough">;
90
+ views: number;
90
91
  description: string;
91
92
  tags: string[];
92
- views: number;
93
93
  scamType: string;
94
94
  reportedBy: string;
95
95
  reportedByAnon: boolean;
@@ -127,9 +127,9 @@ export declare const scammerFirestoreSchema: z.ZodObject<{
127
127
  updatedAt: string | Date | z.objectInputType<{
128
128
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
129
129
  }, z.ZodTypeAny, "passthrough">;
130
+ views: number;
130
131
  description: string;
131
132
  tags: string[];
132
- views: number;
133
133
  scamType: string;
134
134
  reportedBy: string;
135
135
  reportedByAnon: boolean;
@@ -27,9 +27,9 @@ export declare const searchProductItemSchema: z.ZodObject<{
27
27
  currency?: string | undefined;
28
28
  status?: string | undefined;
29
29
  price?: number | undefined;
30
+ listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "bundle" | undefined;
30
31
  mainImage?: string | undefined;
31
32
  featured?: boolean | undefined;
32
- listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "bundle" | undefined;
33
33
  isPromoted?: boolean | undefined;
34
34
  }, {
35
35
  id: string;
@@ -38,9 +38,9 @@ export declare const searchProductItemSchema: z.ZodObject<{
38
38
  currency?: string | undefined;
39
39
  status?: string | undefined;
40
40
  price?: number | undefined;
41
+ listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "bundle" | undefined;
41
42
  mainImage?: string | undefined;
42
43
  featured?: boolean | undefined;
43
- listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "bundle" | undefined;
44
44
  isPromoted?: boolean | undefined;
45
45
  }>;
46
46
  /**
@@ -210,7 +210,24 @@ export async function counterOfferByBuyer(userId, userEmail, input) {
210
210
  const offerCount = await offerRepository.countByBuyerAndProduct(userId, offer.productId, product.updatedAt);
211
211
  if (offerCount >= 3)
212
212
  throw new ValidationError(ERROR_MESSAGES.OFFER.LIMIT_REACHED);
213
- await offerRepository.withdraw(offerId);
213
+ // ── Create BEFORE superseding. Order matters. ───────────────────────────
214
+ //
215
+ // This used to `withdraw()` first and `create()` second. If `create` threw —
216
+ // a validation slip, a Firestore blip — the buyer's live counter was ALREADY
217
+ // withdrawn, no replacement existed, the negotiation was dead, and one of
218
+ // their three offer slots had been burnt for nothing.
219
+ //
220
+ // Reversed, the worst failure is a transient extra pending offer, which
221
+ // `hasActiveOffer` and the expiry sweep both already handle.
222
+ //
223
+ // The chain fields are what make a multi-round negotiation readable at all:
224
+ // `previousOfferId` used to exist ONLY inside the serverLogger call below
225
+ // and never reached Firestore, so three rounds were three unrelated
226
+ // documents. `chainRootOfferId` is denormalised onto every round so a list
227
+ // row can render "Round 2" with zero extra reads, and `findChain` stays a
228
+ // single-field equality served by the automatic index.
229
+ const chainRoot = offer.chainRootOfferId ?? offer.id;
230
+ const nextRound = (offer.counterRound ?? 1) + 1;
214
231
  const newOffer = await offerRepository.create({
215
232
  productId: offer.productId,
216
233
  productTitle: offer.productTitle,
@@ -225,7 +242,18 @@ export async function counterOfferByBuyer(userId, userEmail, input) {
225
242
  listedPrice: offer.listedPrice,
226
243
  currency: offer.currency,
227
244
  buyerNote,
245
+ // NOT carrying `sellerNote` forward: it would render as "Seller note" on
246
+ // the new round, making the seller look like they had already answered a
247
+ // counter they have never seen. Round N-1 keeps its own note and is now
248
+ // reachable via `previousOfferId`.
249
+ previousOfferId: offer.id,
250
+ chainRootOfferId: chainRoot,
251
+ counterRound: nextRound,
228
252
  });
253
+ // Now supersede the old round — one write, one history entry. A `withdrawn`
254
+ // document carrying `supersededByOfferId` renders as *Superseded* (neutral),
255
+ // not *Withdrawn* (negative): the buyer did not walk away, they countered.
256
+ await offerRepository.withdraw(offerId, newOffer.id, { actor: { uid: userId, role: "buyer" } }, offer);
229
257
  const buyerCounterStore = offer.storeId ? await storeRepository.findById(offer.storeId) : null;
230
258
  if (buyerCounterStore?.ownerId)
231
259
  await sendNotification({
@@ -237,12 +265,14 @@ export async function counterOfferByBuyer(userId, userEmail, input) {
237
265
  relatedId: newOffer.id,
238
266
  relatedType: "offer",
239
267
  });
268
+ // `previousOfferId` is no longer logged as its only record — it is a
269
+ // persisted field on `newOffer` now. A log line is not a data model.
240
270
  serverLogger.info("counterOfferByBuyer", {
241
- previousOfferId: offerId,
242
271
  newOfferId: newOffer.id,
243
272
  buyerUid: userId,
244
273
  productId: offer.productId,
245
274
  counterAmount,
275
+ counterRound: nextRound,
246
276
  });
247
277
  return newOffer;
248
278
  }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * OfferPhaseTimeline — the offer-shaped wrapper around `<StatusTimeline>`.
3
+ *
4
+ * An offer is an order with more phases in front of it, so it shares the rail
5
+ * and supplies only its own vocabulary:
6
+ *
7
+ * made → countered → accepted → paid
8
+ * with declined / withdrawn / expired as the terminal-negative branch
9
+ *
10
+ * ## `superseded` is a RENDER KEY, never an OfferStatus
11
+ *
12
+ * A document that is `withdrawn` **and** carries `supersededByOfferId` is not
13
+ * a walk-away — the buyer countered, and the negotiation continued into the
14
+ * next round. It renders neutral ("Superseded"), not negative ("Withdrawn").
15
+ *
16
+ * Deliberately NOT an eighth `OfferStatus` value: `ADMIN_OFFER_STATUS_TABS`
17
+ * would then gain a chip that matches zero stored rows, which is exactly the
18
+ * defect `audit-filter-tab-enums` exists to catch.
19
+ *
20
+ * ## Back-compat
21
+ *
22
+ * Offers written before `statusHistory` existed have none. Branch B derives
23
+ * what it can from the scalars — and `expired` gets **no date at all**,
24
+ * because `expireMany` never wrote one. `updatedAt` is "last write of any
25
+ * kind" and `expiresAt` is a deadline, not an event; using either would be a
26
+ * fabricated timestamp.
27
+ */
28
+ import { type TimelineEntry } from "../../status-history/components/StatusTimeline";
29
+ /** One round of a negotiation, as returned by the API. */
30
+ export interface OfferChainRound {
31
+ id: string;
32
+ counterRound: number;
33
+ status: string;
34
+ offerAmount: number;
35
+ counterAmount?: number;
36
+ superseded: boolean;
37
+ createdAt: string;
38
+ respondedAt?: string;
39
+ sellerNote?: string;
40
+ buyerNote?: string;
41
+ }
42
+ export interface OfferPhaseTimelineProps {
43
+ status: string;
44
+ timeline?: TimelineEntry[];
45
+ timelineTruncated?: number;
46
+ superseded?: boolean;
47
+ createdAt?: string;
48
+ respondedAt?: string;
49
+ acceptedAt?: string;
50
+ paidAt?: string;
51
+ /**
52
+ * Earlier rounds, oldest first. Concatenated ahead of this offer's own
53
+ * steps into ONE rail, each introduced by a "Round N" divider — a
54
+ * negotiation reads as one story, not N disconnected cards.
55
+ */
56
+ chain?: OfferChainRound[];
57
+ className?: string;
58
+ }
59
+ export declare function OfferPhaseTimeline({ status, timeline, timelineTruncated, superseded, createdAt, respondedAt, acceptedAt, paidAt, chain, className, }: OfferPhaseTimelineProps): import("react").JSX.Element;
@@ -0,0 +1,107 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /**
4
+ * OfferPhaseTimeline — the offer-shaped wrapper around `<StatusTimeline>`.
5
+ *
6
+ * An offer is an order with more phases in front of it, so it shares the rail
7
+ * and supplies only its own vocabulary:
8
+ *
9
+ * made → countered → accepted → paid
10
+ * with declined / withdrawn / expired as the terminal-negative branch
11
+ *
12
+ * ## `superseded` is a RENDER KEY, never an OfferStatus
13
+ *
14
+ * A document that is `withdrawn` **and** carries `supersededByOfferId` is not
15
+ * a walk-away — the buyer countered, and the negotiation continued into the
16
+ * next round. It renders neutral ("Superseded"), not negative ("Withdrawn").
17
+ *
18
+ * Deliberately NOT an eighth `OfferStatus` value: `ADMIN_OFFER_STATUS_TABS`
19
+ * would then gain a chip that matches zero stored rows, which is exactly the
20
+ * defect `audit-filter-tab-enums` exists to catch.
21
+ *
22
+ * ## Back-compat
23
+ *
24
+ * Offers written before `statusHistory` existed have none. Branch B derives
25
+ * what it can from the scalars — and `expired` gets **no date at all**,
26
+ * because `expireMany` never wrote one. `updatedAt` is "last write of any
27
+ * kind" and `expiresAt` is a deadline, not an event; using either would be a
28
+ * fabricated timestamp.
29
+ */
30
+ import { StatusTimeline, stepsFromEntries, } from "../../status-history/components/StatusTimeline";
31
+ const PHASE_LABELS = {
32
+ pending: "Offer made",
33
+ countered: "Countered",
34
+ accepted: "Accepted",
35
+ paid: "Paid",
36
+ declined: "Declined",
37
+ withdrawn: "Withdrawn",
38
+ expired: "Expired",
39
+ superseded: "Superseded",
40
+ };
41
+ const NEGATIVE = new Set(["declined", "withdrawn", "expired"]);
42
+ const labelFor = (status) => PHASE_LABELS[status] ?? status;
43
+ const isNegative = (status) => NEGATIVE.has(status);
44
+ /** Branch B — scalars only, for offers with no recorded history. */
45
+ function buildLegacySteps(o) {
46
+ const steps = [
47
+ { key: "pending", label: PHASE_LABELS.pending, date: o.createdAt },
48
+ ];
49
+ const s = (o.status ?? "").toLowerCase();
50
+ if (s === "countered") {
51
+ steps.push({ key: "countered", label: PHASE_LABELS.countered, date: o.respondedAt });
52
+ }
53
+ if (s === "accepted" || s === "paid") {
54
+ steps.push({
55
+ key: "accepted",
56
+ label: PHASE_LABELS.accepted,
57
+ date: o.acceptedAt ?? o.respondedAt,
58
+ });
59
+ }
60
+ if (s === "paid") {
61
+ steps.push({ key: "paid", label: PHASE_LABELS.paid, date: o.paidAt });
62
+ }
63
+ if (s === "declined") {
64
+ steps.push({ key: "declined", label: PHASE_LABELS.declined, date: o.respondedAt, negative: true });
65
+ }
66
+ if (s === "withdrawn") {
67
+ steps.push(o.superseded
68
+ ? { key: "superseded", label: PHASE_LABELS.superseded, date: o.respondedAt, muted: true }
69
+ : { key: "withdrawn", label: PHASE_LABELS.withdrawn, date: o.respondedAt, negative: true });
70
+ }
71
+ if (s === "expired") {
72
+ // No date, on purpose. `expireMany` never wrote one before W2.
73
+ steps.push({ key: "expired", label: PHASE_LABELS.expired, negative: true });
74
+ }
75
+ return steps;
76
+ }
77
+ /** Re-key a withdrawn+superseded step so it reads neutral, not negative. */
78
+ function applySupersede(steps, superseded) {
79
+ if (!superseded)
80
+ return steps;
81
+ return steps.map((s) => s.key === "withdrawn"
82
+ ? { ...s, key: "superseded", label: PHASE_LABELS.superseded, negative: false, muted: true }
83
+ : s);
84
+ }
85
+ export function OfferPhaseTimeline({ status, timeline, timelineTruncated, superseded = false, createdAt, respondedAt, acceptedAt, paidAt, chain, className = "", }) {
86
+ const own = applySupersede(stepsFromEntries(timeline, labelFor, isNegative).length
87
+ ? stepsFromEntries(timeline, labelFor, isNegative)
88
+ : buildLegacySteps({ status, createdAt, respondedAt, acceptedAt, paidAt, superseded }), superseded);
89
+ const priorRounds = (chain ?? []).filter((r) => r.id !== undefined);
90
+ const hasChain = priorRounds.length > 1;
91
+ const steps = hasChain
92
+ ? priorRounds.flatMap((round, idx) => {
93
+ const roundSteps = applySupersede(buildLegacySteps({
94
+ status: round.status,
95
+ createdAt: round.createdAt,
96
+ respondedAt: round.respondedAt,
97
+ superseded: round.superseded,
98
+ }), round.superseded);
99
+ return roundSteps.map((s, i) => ({
100
+ ...s,
101
+ roundLabel: i === 0 ? `Round ${round.counterRound || idx + 1}` : undefined,
102
+ note: i === roundSteps.length - 1 ? (round.sellerNote ?? round.buyerNote) : undefined,
103
+ }));
104
+ })
105
+ : own;
106
+ return (_jsx(StatusTimeline, { title: hasChain ? "Negotiation" : "Offer history", steps: steps, truncatedCount: timelineTruncated, emptyLabel: "No offer history yet", className: className }));
107
+ }
@@ -1,10 +1,11 @@
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 { normalizeError } from "../../../errors/normalize";
4
4
  import { SIEVE_OP, Stack, sieveFilter } from "@mohasinac/appkit/client";
5
5
  import { sortBy } from "@mohasinac/appkit/client";
6
6
  import { useState, useCallback } from "react";
7
- import { Badge, Button, Div, FilterChipGroup, Span, Text, useToast } from "../../../ui";
7
+ import { Badge, Button, Div, FilterChipGroup, RecordDetailModal, Span, Text, useToast } from "../../../ui";
8
+ import { buildBidDetailFields, bidStatusBadge } from "../../auctions/components/bid-detail-fields";
8
9
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
9
10
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
10
11
  import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers";
@@ -29,6 +30,7 @@ const STATUS_BADGE = {
29
30
  cancelled: "danger",
30
31
  };
31
32
  export function SellerBidsView({ endpoint = SELLER_ENDPOINTS.BIDS }) {
33
+ const [detail, setDetail] = useState(null);
32
34
  const { showToast } = useToast();
33
35
  const [collapsedGroups, setCollapsedGroups] = useState(new Set());
34
36
  const toggleGroup = (id) => setCollapsedGroups((s) => {
@@ -130,9 +132,14 @@ export function SellerBidsView({ endpoint = SELLER_ENDPOINTS.BIDS }) {
130
132
  }
131
133
  return (_jsx(Stack, { gap: "3", children: groupedRows.map((group) => {
132
134
  const collapsed = collapsedGroups.has(group.id);
133
- return (_jsxs(Div, { className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "lg", children: [_jsxs(Button, { variant: "ghost", type: "button", onClick: () => toggleGroup(group.id), justify: "between", paddingX: "md", paddingY: "y-xs-tall", textSize: "sm", weight: "semibold", textColor: "default", className: "w-full hover:bg-[var(--appkit-color-surface-raised)]", children: [_jsxs(Span, { className: "truncate", children: [group.title, " ", _jsxs(Span, { size: "xs", weight: "normal", className: "text-[var(--appkit-color-text-muted)]", children: ["\u00B7 ", group.bids.length, " bid", group.bids.length === 1 ? "" : "s"] })] }), _jsx(Span, { "aria-hidden": true, className: "text-[var(--appkit-color-text-muted)]", children: collapsed ? "▸" : "▾" })] }), !collapsed && (_jsx(Div, { className: "border-t border-[var(--appkit-color-border)]", children: _jsx(DataTable, { rows: group.bids, columns: columns, isLoading: false, emptyLabel: "", selectedIds: new Set(selection.selectedIds), onToggleSelect: selection.toggleSelect }) }))] }, group.id));
135
+ return (_jsxs(Div, { className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "lg", children: [_jsxs(Button, { variant: "ghost", type: "button", onClick: () => toggleGroup(group.id), justify: "between", paddingX: "md", paddingY: "y-xs-tall", textSize: "sm", weight: "semibold", textColor: "default", className: "w-full hover:bg-[var(--appkit-color-surface-raised)]", children: [_jsxs(Span, { className: "truncate", children: [group.title, " ", _jsxs(Span, { size: "xs", weight: "normal", className: "text-[var(--appkit-color-text-muted)]", children: ["\u00B7 ", group.bids.length, " bid", group.bids.length === 1 ? "" : "s"] })] }), _jsx(Span, { "aria-hidden": true, className: "text-[var(--appkit-color-text-muted)]", children: collapsed ? "▸" : "▾" })] }), !collapsed && (_jsx(Div, { className: "border-t border-[var(--appkit-color-border)]", children: _jsx(DataTable, { rows: group.bids, columns: columns, isLoading: false, emptyLabel: "", selectedIds: new Set(selection.selectedIds), onToggleSelect: selection.toggleSelect,
136
+ // Without this the row could be selected and bulk-acted
137
+ // on but never opened — a seller could cancel a bid they
138
+ // had no way to read (Root Cause #56).
139
+ onRowClick: (row) => setDetail(row) }) }))] }, group.id));
134
140
  }) }));
135
141
  },
136
142
  };
137
- return _jsx(DataListingView, { config: config });
143
+ const detailBadge = detail ? bidStatusBadge(detail.status) : null;
144
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detail !== null, onClose: () => setDetail(null), title: detail?.productTitle || "Bid", badges: detailBadge ? [{ label: detailBadge.label }] : undefined, fields: detail ? buildBidDetailFields(detail, "seller") : undefined })] }));
138
145
  }
@@ -5,6 +5,8 @@ import { Badge, Button, Checkbox, Div, Form, Heading, IconButton, Input, Label,
5
5
  import { ProductInlineSelect } from "./ProductInlineSelect";
6
6
  import { CategoryInlineSelect } from "./CategoryInlineSelect";
7
7
  import { normalizeError } from "../../../errors/normalize";
8
+ import { FormErrorSummary, applyZodIssues } from "../../../ui/forms";
9
+ import { sellerCouponFormSchema } from "../schemas/coupon-form";
8
10
  const __O = {
9
11
  hidden: "overflow-hidden",
10
12
  };
@@ -39,22 +41,20 @@ export function SellerCouponEditorView({ couponId, initial, onSave, onCancel, })
39
41
  const [saving, setSaving] = useState(false);
40
42
  const [error, setError] = useState(null);
41
43
  const set = (key, value) => setDraft((d) => ({ ...d, [key]: value }));
42
- const handleSubmit = async (e) => {
44
+ const handleSubmit = async (e, setFieldError) => {
43
45
  e.preventDefault();
44
- if (!draft.code.trim()) {
45
- setError("Coupon code is required");
46
- return;
47
- }
48
- if (draft.type !== "free_shipping" && !draft.value) {
49
- setError("Discount value is required");
50
- return;
51
- }
52
- if (!draft.startDate || !draft.endDate) {
53
- setError("Start and end dates are required");
54
- return;
55
- }
56
- if (draft.startDate > draft.endDate) {
57
- setError("Start date must be before end date");
46
+ // Replaces four hand-rolled `if` checks that covered presence and date
47
+ // order and nothing else — a 500% percentage discount, a non-numeric
48
+ // value, a negative minimum spend and a per-customer limit above the
49
+ // total all passed. Errors now land on the field, not in one banner.
50
+ const parsed = sellerCouponFormSchema.safeParse(draft);
51
+ if (!parsed.success) {
52
+ if (setFieldError) {
53
+ applyZodIssues(parsed.error.issues, setFieldError);
54
+ }
55
+ else {
56
+ setError(parsed.error.issues[0]?.message ?? "Please check the form.");
57
+ }
58
58
  return;
59
59
  }
60
60
  setError(null);
@@ -71,11 +71,11 @@ export function SellerCouponEditorView({ couponId, initial, onSave, onCancel, })
71
71
  }
72
72
  };
73
73
  const isEdit = Boolean(couponId);
74
- return (_jsx(Form, { onSubmit: handleSubmit, noValidate: true, children: _jsxs(Stack, { gap: "none", className: `max-w-lg mx-auto border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] ${__O.hidden}`, rounded: "xl", shadow: "sm", children: [_jsx(Div, { className: "h-[3px] w-full [background:linear-gradient(to_right,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_55%,var(--appkit-color-secondary-400)_100%)]", "aria-hidden": "true" }), _jsx(Div, { border: "bottom", paddingY: "y-md-lg", padding: "x-lg", children: _jsxs(Row, { justify: "between", gap: "3", children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text)]", size: "lg", weight: "semibold", children: isEdit ? "Edit Coupon" : "Create Coupon" }), isEdit && (_jsx(Badge, { variant: draft.isActive ? "success" : "default", children: draft.isActive ? "Active" : "Inactive" }))] }) }), _jsxs(Stack, { gap: "5", padding: "lg", children: [error && (_jsx(Div, { textSize: "sm", className: "border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "lg", children: error })), _jsx(Input, { label: "Coupon Code", value: draft.code, onChange: (e) => set("code", e.target.value.toUpperCase().replace(/\s+/g, "")), placeholder: "e.g. WELCOME10", required: true, disabled: isEdit, helperText: isEdit ? "Code cannot be changed after creation" : "Customers enter this at checkout" }), _jsx(Select, { label: "Discount Type", value: draft.type, options: TYPE_OPTIONS, onChange: (e) => set("type", e.target.value) }), draft.type !== "free_shipping" && (_jsx(Input, { label: draft.type === "percentage" ? "Discount Percentage (%)" : "Discount Amount (₹)", type: "number", min: 1, max: draft.type === "percentage" ? 100 : undefined, value: draft.value, onChange: (e) => set("value", e.target.value), placeholder: draft.type === "percentage" ? "e.g. 10" : "e.g. 50", required: true, helperText: draft.type === "percentage"
74
+ return (_jsx(Form, { schema: sellerCouponFormSchema, onSubmit: (e) => e.preventDefault(), children: ({ setFieldError }) => (_jsxs(Stack, { gap: "none", className: `max-w-lg mx-auto border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] ${__O.hidden}`, rounded: "xl", shadow: "sm", children: [_jsx(Div, { className: "h-[3px] w-full [background:linear-gradient(to_right,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_55%,var(--appkit-color-secondary-400)_100%)]", "aria-hidden": "true" }), _jsx(Div, { border: "bottom", paddingY: "y-md-lg", padding: "x-lg", children: _jsxs(Row, { justify: "between", gap: "3", children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text)]", size: "lg", weight: "semibold", children: isEdit ? "Edit Coupon" : "Create Coupon" }), isEdit && (_jsx(Badge, { variant: draft.isActive ? "success" : "default", children: draft.isActive ? "Active" : "Inactive" }))] }) }), _jsxs(Stack, { gap: "5", padding: "lg", children: [_jsx(FormErrorSummary, {}), error && (_jsx(Div, { textSize: "sm", className: "border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "lg", children: error })), _jsx(Input, { label: "Coupon Code", value: draft.code, onChange: (e) => set("code", e.target.value.toUpperCase().replace(/\s+/g, "")), placeholder: "e.g. WELCOME10", required: true, disabled: isEdit, helperText: isEdit ? "Code cannot be changed after creation" : "Customers enter this at checkout" }), _jsx(Select, { label: "Discount Type", value: draft.type, options: TYPE_OPTIONS, onChange: (e) => set("type", e.target.value) }), draft.type !== "free_shipping" && (_jsx(Input, { label: draft.type === "percentage" ? "Discount Percentage (%)" : "Discount Amount (₹)", type: "number", min: 1, max: draft.type === "percentage" ? 100 : undefined, value: draft.value, onChange: (e) => set("value", e.target.value), placeholder: draft.type === "percentage" ? "e.g. 10" : "e.g. 50", required: true, helperText: draft.type === "percentage"
75
75
  ? "Enter a value between 1 and 100"
76
76
  : "Fixed rupee discount applied to the order" })), draft.type === "percentage" && (_jsx(Input, { label: "Max Discount Cap (\u20B9, optional)", type: "number", min: 0, value: draft.maxDiscount, onChange: (e) => set("maxDiscount", e.target.value), placeholder: "Leave blank for no cap", helperText: "Maximum rupee discount regardless of percentage" })), _jsx(Input, { label: "Minimum Order Amount (\u20B9, optional)", type: "number", min: 0, value: draft.minPurchase, onChange: (e) => set("minPurchase", e.target.value), placeholder: "Leave blank for no minimum" }), _jsxs(Div, { children: [_jsx(Text, { size: "sm", className: CLS_SECTION_LABEL, children: "Usage Limits" }), _jsxs(Div, { layout: "grid", gap: "3", className: "grid-cols-2", children: [_jsx(Input, { label: "Total Uses", type: "number", min: 0, value: draft.totalLimit, onChange: (e) => set("totalLimit", e.target.value), placeholder: "0 = unlimited" }), _jsx(Input, { label: "Per Customer", type: "number", min: 0, value: draft.perUserLimit, onChange: (e) => set("perUserLimit", e.target.value), placeholder: "0 = unlimited" })] })] }), _jsxs(Div, { children: [_jsx(Text, { size: "sm", className: CLS_SECTION_LABEL, children: "Validity Period" }), _jsxs(Div, { layout: "grid", gap: "3", className: "grid-cols-2", children: [_jsx(Input, { label: "Start Date", type: "date", value: draft.startDate, onChange: (e) => set("startDate", e.target.value), required: true }), _jsx(Input, { label: "End Date", type: "date", value: draft.endDate, onChange: (e) => set("endDate", e.target.value), required: true })] })] }), _jsxs(Div, { children: [_jsx(Text, { size: "sm", className: CLS_SECTION_LABEL, children: "Applicability (optional)" }), _jsxs(Stack, { gap: "sm", children: [_jsxs(Div, { children: [_jsx(Text, { size: "xs", className: "mb-1 text-[var(--appkit-color-text-secondary)]", children: "Applicable products" }), _jsx(ProductInlineSelect, { scope: "store", multiple: true, value: draft.applicableProducts, onChange: (v) => set("applicableProducts", v), placeholder: "Restrict to specific products\u2026" })] }), _jsxs(Div, { children: [_jsx(Text, { size: "xs", className: "mb-1 text-[var(--appkit-color-text-secondary)]", children: "Applicable categories" }), _jsx(CategoryInlineSelect, { value: "", onChange: (id) => {
77
77
  if (!id || draft.applicableCategories.includes(id))
78
78
  return;
79
79
  set("applicableCategories", [...draft.applicableCategories, id]);
80
- }, placeholder: "Add a category\u2026" }), draft.applicableCategories.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", padding: "t-xs", children: draft.applicableCategories.map((cid) => (_jsxs(Span, { layout: "inline-flex", gap: "xs", border: "strong", padding: "pill-sm", rounded: "full", surface: "muted", color: "primary", size: "xs", children: [cid, _jsx(IconButton, { type: "button", "aria-label": `Remove ${cid}`, variant: "ghost", className: "text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text)]", onClick: () => set("applicableCategories", draft.applicableCategories.filter((c) => c !== cid)), children: "\u00D7" })] }, cid))) }))] }), _jsx(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: "Leave both empty to apply the coupon to every product in your store." })] })] }), _jsx(Row, { className: "border border-[var(--appkit-color-border)] dark:border-[var(--appkit-color-border-dark)]", padding: "inline", align: "center", gap: "3", rounded: "lg", children: _jsxs(Label, { layout: "flex", gap: "lg", className: "cursor-pointer w-full", children: [_jsx(Checkbox, { bare: true, checked: draft.isActive, onChange: (e) => set("isActive", e.target.checked), className: "h-4 w-4 rounded border-[var(--appkit-color-border)] text-[var(--appkit-color-primary)] focus:ring-[var(--appkit-color-primary)]" }), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-primary)]", weight: "medium", children: "Active" }), _jsx(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: "Customers can apply this coupon at checkout" })] })] }) })] }), _jsx(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "inlineLg", children: _jsxs(Row, { justify: "end", gap: "3", children: [onCancel && (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: saving, children: "Cancel" })), _jsx(Button, { type: "submit", isLoading: saving, disabled: saving, children: isEdit ? "Save Changes" : "Create Coupon" })] }) })] }) }));
80
+ }, placeholder: "Add a category\u2026" }), draft.applicableCategories.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", padding: "t-xs", children: draft.applicableCategories.map((cid) => (_jsxs(Span, { layout: "inline-flex", gap: "xs", border: "strong", padding: "pill-sm", rounded: "full", surface: "muted", color: "primary", size: "xs", children: [cid, _jsx(IconButton, { type: "button", "aria-label": `Remove ${cid}`, variant: "ghost", className: "text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text)]", onClick: () => set("applicableCategories", draft.applicableCategories.filter((c) => c !== cid)), children: "\u00D7" })] }, cid))) }))] }), _jsx(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: "Leave both empty to apply the coupon to every product in your store." })] })] }), _jsx(Row, { className: "border border-[var(--appkit-color-border)] dark:border-[var(--appkit-color-border-dark)]", padding: "inline", align: "center", gap: "3", rounded: "lg", children: _jsxs(Label, { layout: "flex", gap: "lg", className: "cursor-pointer w-full", children: [_jsx(Checkbox, { bare: true, checked: draft.isActive, onChange: (e) => set("isActive", e.target.checked), className: "h-4 w-4 rounded border-[var(--appkit-color-border)] text-[var(--appkit-color-primary)] focus:ring-[var(--appkit-color-primary)]" }), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-primary)]", weight: "medium", children: "Active" }), _jsx(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: "Customers can apply this coupon at checkout" })] })] }) })] }), _jsx(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "inlineLg", children: _jsxs(Row, { justify: "end", gap: "3", children: [onCancel && (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: saving, children: "Cancel" })), _jsx(Button, { type: "submit", isLoading: saving, disabled: saving, onClick: (e) => void handleSubmit(e, setFieldError), children: isEdit ? "Save Changes" : "Create Coupon" })] }) })] })) }));
81
81
  }
@@ -5,6 +5,7 @@ import { sortBy } from "@mohasinac/appkit/client";
5
5
  import React from "react";
6
6
  import { FilterChipGroup, ListingLayout, RowActionMenu, RecordDetailModal } from "../../../ui";
7
7
  import { QuickFormDrawer } from "../../shell/QuickFormDrawer";
8
+ import { OfferPhaseTimeline } from "./OfferPhaseTimeline";
8
9
  import { counterOfferFormSchema } from "../schemas/offer-forms";
9
10
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
10
11
  import { SELLER_OFFER_STATUS_TABS } from "../../admin/constants/filter-tabs";
@@ -79,7 +80,7 @@ export function SellerOffersView({ children, onAcceptOffer, onRejectOffer, onCou
79
80
  ? [{ label: "Buyer must pay by", value: toRelativeDate(d.checkoutDeadline) }]
80
81
  : []),
81
82
  ...(d.sellerNote ? [{ label: "Your note", value: toStringValue(d.sellerNote, "") }] : []),
82
- ] }), _jsx(QuickFormDrawer, { isOpen: counterRow !== null, onClose: () => setCounterRow(null), title: `Counter offer — ${counterRow?.primary ?? ""}`, submitLabel: "Send counter", isLoading: isCountering, schema: counterOfferFormSchema, fields: [
83
+ ], extra: _jsx(OfferPhaseTimeline, { status: toStringValue(d.status, "pending"), timeline: d.timeline, timelineTruncated: d.timelineTruncated, superseded: Boolean(d.superseded), createdAt: toStringValue(d.createdAt, ""), respondedAt: toStringValue(d.respondedAt, ""), acceptedAt: toStringValue(d.acceptedAt, ""), paidAt: toStringValue(d.paidAt, ""), chain: d.chain }) }), _jsx(QuickFormDrawer, { isOpen: counterRow !== null, onClose: () => setCounterRow(null), title: `Counter offer — ${counterRow?.primary ?? ""}`, submitLabel: "Send counter", isLoading: isCountering, schema: counterOfferFormSchema, fields: [
83
84
  {
84
85
  name: "counterAmount",
85
86
  label: "Your counter price (₹)",
@@ -5,7 +5,7 @@ import { z } from "zod";
5
5
  import { Alert, Badge, Checkbox, Div, FormField, FormGroup, Heading, Label, Row, Stack, Text, Toggle } from "../../../ui";
6
6
  import { StackedViewShell } from "../../../ui";
7
7
  import { FormShellContext, useFormShellState, applyZodIssues, FormErrorSummary } from "../../../ui/forms";
8
- import { SectionForm, useSectionFormNav } from "../../shell";
8
+ import { SectionForm, useSectionFormNav, annotate } from "../../shell";
9
9
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
10
10
  import { normalizeError } from "../../../errors/normalize";
11
11
  const __P = {
@@ -16,20 +16,44 @@ const __P = {
16
16
  // accountNumber field's helpText below), and the exact keep-existing
17
17
  // semantics live server-side; a client-side format requirement risks
18
18
  // blocking that legitimate blank-resave case.
19
+ /**
20
+ * Section membership and row span live on the FIELD (W3).
21
+ *
22
+ * The `sections` array below still hand-writes this form's JSX, because its
23
+ * Payout Method panel is genuinely bespoke — a configured/not-configured
24
+ * badge, a "current UPI/bank" alert, and a UPI-vs-bank branch. What is NOT
25
+ * hand-written any more is which field belongs where and how wide it is:
26
+ * that is read off the schema, and `audit-form-field-roundtrip` (W4) can
27
+ * cross-check the two so they cannot drift apart.
28
+ *
29
+ * 🛑 `annotate()` must be the OUTERMOST call on each field. It keys a WeakMap
30
+ * by schema instance, and every zod wrapper returns a NEW instance — so
31
+ * `annotate(z.string(), …).optional()` silently loses the metadata and the
32
+ * field falls into the "advanced" fallback section. `audit-field-ui-meta`
33
+ * blocks that ordering.
34
+ */
19
35
  const payoutSettingsDraftSchema = z.object({
20
- method: z.enum(["upi", "bank_transfer"]),
21
- upiId: z.string(),
22
- accountHolderName: z.string(),
23
- accountNumber: z.string(),
24
- ifscCode: z.string(),
25
- bankName: z.string(),
26
- accountType: z.enum(["savings", "current"]),
27
- gstin: z.string(),
28
- pan: z.string(),
29
- businessType: z.string(),
30
- autoPayout: z.boolean(),
31
- minimumThreshold: z.string(),
32
- emiEnabled: z.boolean(),
36
+ method: annotate(z.enum(["upi", "bank_transfer"]), {
37
+ section: "method", sectionLabel: "Payout Method", sectionRequired: true,
38
+ quick: true, order: 1, row: "pair",
39
+ }),
40
+ upiId: annotate(z.string(), { section: "method", quick: true, order: 2, row: "pair" }),
41
+ accountHolderName: annotate(z.string(), { section: "method", quick: true, order: 3, row: "full" }),
42
+ // Account number and IFSC read as one unit and each needs about half the
43
+ // width — the pairing this whole mechanism exists to make declarative
44
+ // instead of a bare `grid-cols-2` that is two columns wide at 320px.
45
+ accountNumber: annotate(z.string(), { section: "method", quick: true, order: 4, row: "pair" }),
46
+ ifscCode: annotate(z.string(), { section: "method", quick: true, order: 5, row: "pair" }),
47
+ bankName: annotate(z.string(), { section: "method", quick: true, order: 6, row: "pair" }),
48
+ accountType: annotate(z.enum(["savings", "current"]), { section: "method", quick: true, order: 7, row: "pair" }),
49
+ gstin: annotate(z.string(), { section: "tax", sectionLabel: "Tax Info", order: 1, row: "pair" }),
50
+ pan: annotate(z.string(), { section: "tax", order: 2, row: "pair" }),
51
+ businessType: annotate(z.string(), { section: "tax", order: 3, row: "pair" }),
52
+ autoPayout: annotate(z.boolean(), {
53
+ section: "preferences", sectionLabel: "Preferences", order: 1, row: "quarter",
54
+ }),
55
+ minimumThreshold: annotate(z.string(), { section: "preferences", order: 2, row: "pair" }),
56
+ emiEnabled: annotate(z.boolean(), { section: "preferences", order: 3, row: "quarter" }),
33
57
  }).superRefine((v, ctx) => {
34
58
  if (v.method === "upi") {
35
59
  if (!v.upiId.trim()) {