@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.
- package/dist/_internal/server/features/auctions/actions.js +2 -2
- package/dist/_internal/server/features/cart/service.d.ts +3 -3
- package/dist/_internal/server/features/checkout/actions.js +19 -0
- package/dist/_internal/server/features/checkout/source-context.d.ts +5 -0
- package/dist/_internal/server/features/checkout/source-context.js +157 -0
- package/dist/_internal/server/features/offers/adapters.d.ts +103 -0
- package/dist/_internal/server/features/offers/adapters.js +88 -0
- package/dist/_internal/server/features/offers/index.d.ts +2 -0
- package/dist/_internal/server/features/offers/index.js +1 -0
- package/dist/_internal/server/features/orders/adapters.js +24 -0
- package/dist/_internal/server/jobs/core/offerExpiry.js +10 -10
- package/dist/_internal/shared/actions/action-registry.d.ts +9 -2
- package/dist/_internal/shared/actions/action-registry.js +458 -7
- package/dist/_internal/shared/features/auctions/schema.d.ts +14 -2
- package/dist/_internal/shared/features/auctions/schema.js +13 -1
- package/dist/_internal/shared/features/blog/schema.d.ts +4 -4
- package/dist/_internal/shared/features/cart/schema.d.ts +25 -20
- package/dist/_internal/shared/features/cart/schema.js +15 -0
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +22 -22
- package/dist/_internal/shared/features/orders/schema.d.ts +4 -4
- package/dist/_internal/shared/features/products/schema.d.ts +27 -27
- package/dist/_internal/shared/features/promotions/schema.d.ts +4 -4
- package/dist/_internal/shared/features/reviews/schema.d.ts +2 -2
- package/dist/_internal/shared/history/history.d.ts +22 -0
- package/dist/_internal/shared/history/history.js +41 -1
- package/dist/client.d.ts +13 -2
- package/dist/client.js +20 -2
- package/dist/constants/field-names.d.ts +8 -6
- package/dist/constants/field-names.js +48 -6
- package/dist/contracts/extend.d.ts +29 -0
- package/dist/contracts/extend.js +20 -0
- package/dist/contracts/index.d.ts +2 -1
- package/dist/contracts/index.js +2 -0
- package/dist/features/account/components/UserBidsView.js +12 -4
- package/dist/features/account/components/UserReturnsView.d.ts +3 -0
- package/dist/features/account/components/UserReturnsView.js +17 -1
- package/dist/features/admin/components/AdminAuditLogView.js +1 -0
- package/dist/features/admin/components/AdminBidsView.js +15 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +65 -33
- package/dist/features/admin/components/AdminFeatureEditorView.js +35 -6
- package/dist/features/admin/components/AdminNavEditorView.js +15 -5
- package/dist/features/admin/components/AdminNewsletterView.js +18 -2
- package/dist/features/admin/components/AdminOffersView.js +62 -18
- package/dist/features/admin/components/AdminPaymentMethodsView.js +24 -4
- package/dist/features/admin/components/AdminScammerEditorView.d.ts +2 -1
- package/dist/features/admin/components/AdminScammerEditorView.js +3 -13
- package/dist/features/admin/components/AdminScammersView.js +3 -8
- package/dist/features/admin/components/AdminSectionsView.js +32 -23
- package/dist/features/admin/components/AdminSessionsView.js +17 -2
- package/dist/features/admin/components/AdminViewCards.js +24 -17
- package/dist/features/admin/components/DataListingView.js +15 -1
- package/dist/features/admin/components/DataTable.js +7 -17
- package/dist/features/admin/components/ViewAuditLogEntryModal.js +1 -0
- package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +4 -2
- package/dist/features/admin/components/sections/adminSectionsTypes.js +45 -27
- package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
- package/dist/features/admin/constants/filter-tabs.js +1 -0
- package/dist/features/admin/schemas/homepage-section-form.d.ts +22 -0
- package/dist/features/admin/schemas/homepage-section-form.js +68 -0
- package/dist/features/admin/schemas/nav-item-form.d.ts +74 -0
- package/dist/features/admin/schemas/nav-item-form.js +72 -0
- package/dist/features/auctions/components/AuctionBidsTable.d.ts +10 -1
- package/dist/features/auctions/components/AuctionBidsTable.js +22 -4
- package/dist/features/auctions/components/bid-detail-fields.d.ts +29 -0
- package/dist/features/auctions/components/bid-detail-fields.js +103 -0
- package/dist/features/auctions/schemas/index.d.ts +14 -14
- package/dist/features/audit-log/schemas/firestore.d.ts +14 -0
- package/dist/features/audit-log/schemas/firestore.js +7 -0
- package/dist/features/auth/schemas/index.d.ts +2 -2
- package/dist/features/blog/actions/blog-actions.d.ts +7 -7
- package/dist/features/blog/schemas/blog-form.d.ts +80 -0
- package/dist/features/blog/schemas/blog-form.js +100 -0
- package/dist/features/blog/schemas/index.d.ts +10 -10
- package/dist/features/cart/schemas/index.d.ts +3 -3
- package/dist/features/catalogue/schemas/validation.d.ts +12 -12
- package/dist/features/categories/schemas/index.d.ts +16 -16
- package/dist/features/collections/schemas/index.d.ts +8 -8
- package/dist/features/events/components/AdminEventEditorView.js +32 -54
- package/dist/features/events/components/AdminEventEntriesView.js +22 -3
- package/dist/features/events/schemas/event-form.d.ts +113 -0
- package/dist/features/events/schemas/event-form.js +82 -0
- package/dist/features/events/schemas/index.d.ts +2 -2
- package/dist/features/faq/actions/faq-actions.d.ts +6 -6
- package/dist/features/faq/schemas/index.d.ts +6 -6
- package/dist/features/grouped/schemas/grouped-listing-form.d.ts +101 -0
- package/dist/features/grouped/schemas/grouped-listing-form.js +89 -0
- package/dist/features/grouped/schemas/index.d.ts +4 -4
- package/dist/features/homepage/actions/homepage-section-actions.d.ts +11 -3
- package/dist/features/homepage/actions/homepage-section-actions.js +33 -1
- package/dist/features/homepage/schemas/index.d.ts +71 -40
- package/dist/features/homepage/schemas/index.js +24 -0
- package/dist/features/messages/schemas/index.d.ts +2 -2
- package/dist/features/orders/components/OrderStatusTimeline.d.ts +5 -1
- package/dist/features/orders/components/OrderStatusTimeline.js +39 -20
- package/dist/features/orders/repository/orders.repository.js +5 -0
- package/dist/features/orders/schemas/firestore.d.ts +36 -1
- package/dist/features/orders/schemas/index.d.ts +24 -24
- package/dist/features/orders/types/index.d.ts +35 -7
- package/dist/features/pre-orders/schemas/index.d.ts +4 -4
- package/dist/features/products/components/GroupSettingsPanel.js +5 -1
- package/dist/features/products/schemas/index.d.ts +38 -38
- package/dist/features/products/schemas/product-features.validators.d.ts +75 -2
- package/dist/features/products/schemas/product-features.validators.js +63 -0
- package/dist/features/promotions/schemas/index.d.ts +14 -14
- package/dist/features/reviews/schemas/index.d.ts +14 -14
- package/dist/features/scams/constants/scammer-status-display.d.ts +28 -0
- package/dist/features/scams/constants/scammer-status-display.js +79 -0
- package/dist/features/scams/schemas/index.d.ts +2 -2
- package/dist/features/search/schemas/index.d.ts +2 -2
- package/dist/features/seller/actions/offer-actions.js +32 -2
- package/dist/features/seller/components/OfferPhaseTimeline.d.ts +59 -0
- package/dist/features/seller/components/OfferPhaseTimeline.js +107 -0
- package/dist/features/seller/components/SellerBidsView.js +11 -4
- package/dist/features/seller/components/SellerCouponEditorView.js +17 -17
- package/dist/features/seller/components/SellerOffersView.js +2 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +38 -14
- package/dist/features/seller/repository/offer.repository.d.ts +69 -8
- package/dist/features/seller/repository/offer.repository.js +127 -23
- package/dist/features/seller/schemas/coupon-form.d.ts +72 -0
- package/dist/features/seller/schemas/coupon-form.js +116 -0
- package/dist/features/seller/schemas/firestore.d.ts +56 -2
- package/dist/features/seller/schemas/firestore.js +27 -0
- package/dist/features/seller/schemas/index.d.ts +2 -2
- package/dist/features/seller/schemas/offer-forms.d.ts +21 -0
- package/dist/features/seller/schemas/offer-forms.js +21 -0
- package/dist/features/shell/build-sections.d.ts +63 -0
- package/dist/features/shell/build-sections.js +207 -0
- package/dist/features/shell/field-ui-meta.d.ts +115 -0
- package/dist/features/shell/field-ui-meta.js +324 -0
- package/dist/features/shell/index.d.ts +4 -0
- package/dist/features/shell/index.js +2 -0
- package/dist/features/shipments/schemas/validation.d.ts +22 -22
- package/dist/features/status-history/components/StatusTimeline.d.ts +74 -0
- package/dist/features/status-history/components/StatusTimeline.js +46 -0
- package/dist/features/status-history/index.d.ts +11 -0
- package/dist/features/status-history/index.js +10 -0
- package/dist/features/store-extensions/components/ReviewDecisionModal.d.ts +27 -0
- package/dist/features/store-extensions/components/ReviewDecisionModal.js +82 -0
- package/dist/features/store-extensions/index.d.ts +12 -0
- package/dist/features/store-extensions/index.js +12 -0
- package/dist/features/store-extensions/schemas/admin-notification-form.d.ts +36 -0
- package/dist/features/store-extensions/schemas/admin-notification-form.js +114 -0
- package/dist/features/store-extensions/schemas/analytics-forms.d.ts +57 -0
- package/dist/features/store-extensions/schemas/analytics-forms.js +158 -0
- package/dist/features/store-extensions/schemas/custom-role-form.d.ts +85 -0
- package/dist/features/store-extensions/schemas/custom-role-form.js +118 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +21 -1
- package/dist/features/store-extensions/schemas/firestore.js +60 -0
- package/dist/features/store-extensions/schemas/google-config-form.d.ts +53 -0
- package/dist/features/store-extensions/schemas/google-config-form.js +105 -0
- package/dist/features/store-extensions/schemas/item-request-create-form.d.ts +28 -0
- package/dist/features/store-extensions/schemas/item-request-create-form.js +80 -0
- package/dist/features/store-extensions/schemas/listing-template-form.d.ts +78 -0
- package/dist/features/store-extensions/schemas/listing-template-form.js +87 -0
- package/dist/features/store-extensions/schemas/moderation-review-form.d.ts +49 -0
- package/dist/features/store-extensions/schemas/moderation-review-form.js +101 -0
- package/dist/features/store-extensions/schemas/payout-method-form.d.ts +153 -0
- package/dist/features/store-extensions/schemas/payout-method-form.js +155 -0
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +25 -0
- package/dist/features/store-extensions/schemas/report-create-form.js +79 -0
- package/dist/features/store-extensions/schemas/report-review-form.d.ts +47 -0
- package/dist/features/store-extensions/schemas/report-review-form.js +111 -0
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +148 -0
- package/dist/features/store-extensions/schemas/shipping-config-form.js +104 -0
- package/dist/features/store-extensions/schemas/store-category-form.d.ts +54 -0
- package/dist/features/store-extensions/schemas/store-category-form.js +73 -0
- package/dist/features/stores/schemas/index.d.ts +4 -4
- package/dist/features/support/schemas/index.d.ts +4 -4
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +350 -0
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.js +12 -2
- package/dist/next/routing/route-map.d.ts +16 -2
- package/dist/next/routing/route-map.js +9 -2
- package/dist/schemas/firestore-value.d.ts +8 -0
- package/dist/schemas/firestore-value.js +44 -0
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/registry.d.ts +734 -101
- package/dist/schemas/registry.js +64 -1
- package/dist/seed/bids-seed-data.js +30 -0
- package/dist/seed/offers-seed-data.js +212 -7
- package/dist/seed/orders-seed-data.js +336 -1
- package/dist/seed/products-auctions-seed-data.js +41 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +5 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/DataTable.js +4 -2
- package/dist/ui/columns/build-columns.d.ts +6 -1
- package/dist/ui/columns/build-columns.js +53 -0
- package/dist/ui/columns/cell-renderers.d.ts +68 -0
- package/dist/ui/columns/cell-renderers.js +111 -0
- package/dist/ui/columns/column-renderers.d.ts +6 -1
- package/dist/ui/columns/column-renderers.js +6 -1
- package/dist/ui/columns/index.d.ts +2 -1
- package/dist/ui/columns/index.js +5 -1
- package/dist/ui/components/Button.js +27 -2
- package/dist/ui/components/RecordDetailModal.d.ts +10 -1
- package/dist/ui/components/RecordDetailModal.js +2 -2
- package/dist/ui/icons/icon-registry.d.ts +65 -0
- package/dist/ui/icons/icon-registry.js +115 -0
- package/package.json +2 -1
- package/scripts/node-esm-loader.mjs +65 -0
- package/scripts/seed-cli-loader.mjs +6 -49
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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, {
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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()) {
|