@mohasinac/appkit 4.42.1 → 4.42.2
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/offers/adapters.d.ts +29 -7
- package/dist/_internal/server/features/offers/adapters.js +14 -3
- package/dist/_internal/server/features/orders/adapters.js +54 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.js +1 -0
- package/dist/features/account/components/UserOrdersView.js +30 -2
- package/dist/features/account/hooks/useAddresses.d.ts +21 -6
- package/dist/features/account/hooks/useAddresses.js +1 -1
- package/dist/features/addresses/hooks/usePostalLookup.js +84 -12
- package/dist/features/admin/components/AdminCategoriesView.js +37 -2
- package/dist/features/admin/components/AdminOffersView.js +2 -2
- package/dist/features/admin/constants/filter-tabs.d.ts +25 -0
- package/dist/features/admin/constants/filter-tabs.js +30 -0
- package/dist/features/admin/hooks/useAdminListingData.d.ts +35 -0
- package/dist/features/admin/hooks/useAdminListingData.js +45 -9
- package/dist/features/cart/schemas/index.d.ts +15 -0
- package/dist/features/cart/schemas/index.js +2 -0
- package/dist/features/cart/types/index.d.ts +22 -0
- package/dist/features/categories/components/CategoriesIndexListing.js +19 -1
- package/dist/features/categories/components/CategoriesIndexPageView.js +14 -1
- package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +8 -0
- package/dist/features/classified/components/ClassifiedDetailPageView.js +1 -0
- package/dist/features/events/actions/event-actions.d.ts +13 -0
- package/dist/features/events/actions/event-actions.js +75 -1
- package/dist/features/events/repository/event-entry.repository.d.ts +17 -0
- package/dist/features/events/repository/event-entry.repository.js +36 -0
- package/dist/features/homepage/components/BrandsSection.js +1 -1
- package/dist/features/homepage/components/CustomerReviewsSection.js +1 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +1 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/media/upload/MediaUploadField.js +21 -3
- package/dist/features/orders/components/OrdersList.js +2 -1
- package/dist/features/orders/index.d.ts +1 -0
- package/dist/features/orders/index.js +1 -0
- package/dist/features/orders/types/index.d.ts +2 -0
- package/dist/features/orders/utils/order-ref.d.ts +45 -0
- package/dist/features/orders/utils/order-ref.js +58 -0
- package/dist/features/products/components/MakeOfferButton.d.ts +18 -1
- package/dist/features/products/components/MakeOfferButton.js +23 -1
- package/dist/features/products/components/ProductDetailPageView.d.ts +5 -0
- package/dist/features/products/components/ProductDetailPageView.js +3 -2
- package/dist/features/products/repository/products.repository.js +29 -2
- package/dist/features/seller/actions/offer-actions.js +32 -0
- package/dist/features/seller/components/QuickProductForm.js +15 -1
- package/dist/features/seller/components/SellerBidsView.js +3 -1
- package/dist/features/seller/components/SellerOffersView.js +2 -2
- package/dist/features/seller/components/SellerOrdersView.js +49 -6
- package/dist/features/seller/components/SellerProductsCards.js +23 -1
- package/dist/features/seller/components/SellerProductsView.js +27 -2
- package/dist/features/seller/hooks/useSellerListingData.d.ts +0 -1
- package/dist/features/seller/hooks/useSellerListingData.js +17 -19
- package/dist/features/shell/SectionForm.js +33 -2
- package/dist/features/tester/seed-data/_happy-path.d.ts +23 -0
- package/dist/features/tester/seed-data/_happy-path.js +710 -0
- package/dist/features/tester/seed-data/_money-flows.js +34 -23
- package/dist/features/tester/seed-data/authored/account-auth__profile-settings.js +22 -3
- package/dist/features/tester/seed-data/authored/addresses__address-filters.js +17 -12
- package/dist/features/tester/seed-data/authored/addresses__postal-lookup.js +7 -6
- package/dist/features/tester/seed-data/authored/addresses__postal-validation.js +10 -4
- package/dist/features/tester/seed-data/authored/addresses__state-picker.js +13 -8
- package/dist/features/tester/seed-data/authored/addresses__unban-request.js +49 -11
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +10 -6
- package/dist/features/tester/seed-data/authored/admin__bans-and-trust.js +71 -32
- package/dist/features/tester/seed-data/authored/admin__blog-faqs.js +80 -31
- package/dist/features/tester/seed-data/authored/admin__bug-hunter-rewards.js +62 -25
- package/dist/features/tester/seed-data/authored/admin__bulk-actions.js +61 -31
- package/dist/features/tester/seed-data/authored/admin__bundles.js +84 -37
- package/dist/features/tester/seed-data/authored/admin__buyer-data-admin.js +90 -57
- package/dist/features/tester/seed-data/authored/admin__catalog-listings.js +38 -26
- package/dist/features/tester/seed-data/authored/admin__category-brand-authoring.js +47 -28
- package/dist/features/tester/seed-data/authored/admin__classifieds-digitalcodes-live.js +118 -31
- package/dist/features/tester/seed-data/authored/admin__content-deletes.js +30 -24
- package/dist/features/tester/seed-data/authored/admin__content-marketing.js +83 -46
- package/dist/features/tester/seed-data/authored/admin__coupons.js +8 -8
- package/dist/features/tester/seed-data/authored/admin__events-raffles-spin.js +9 -9
- package/dist/features/tester/seed-data/authored/admin__firebase-function-effects.js +19 -4
- package/dist/features/tester/seed-data/authored/admin__media-watermark.js +77 -33
- package/dist/features/tester/seed-data/authored/admin__orders-fulfillment.js +5 -5
- package/dist/features/tester/seed-data/authored/admin__prize-draws-lotteries.js +29 -5
- package/dist/features/tester/seed-data/authored/admin__site-system.js +45 -23
- package/dist/features/tester/seed-data/authored/admin__uncovered-admin-pages.js +53 -28
- package/dist/features/tester/seed-data/authored/admin__users-trust.js +20 -20
- package/dist/features/tester/seed-data/authored/buying__bidding.js +19 -2
- package/dist/features/tester/seed-data/authored/buying__browsing-search.js +20 -5
- package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +28 -10
- package/dist/features/tester/seed-data/authored/buying__cart.js +10 -3
- package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +0 -16
- package/dist/features/tester/seed-data/authored/buying__my-orders.js +27 -5
- package/dist/features/tester/seed-data/authored/buying__offers.js +22 -7
- package/dist/features/tester/seed-data/authored/buying__order-detail-actions.js +3 -3
- package/dist/features/tester/seed-data/authored/buying__product-detail.js +35 -4
- package/dist/features/tester/seed-data/authored/buying__return-request.js +19 -6
- package/dist/features/tester/seed-data/authored/buying__reviews-pagination.js +36 -18
- package/dist/features/tester/seed-data/authored/buying__reviews.js +16 -1
- package/dist/features/tester/seed-data/authored/buying__user-dashboard-navigation.js +2 -1
- package/dist/features/tester/seed-data/authored/buying__user-uncovered-pages.js +16 -1
- package/dist/features/tester/seed-data/authored/community-support__public-profile.js +3 -2
- package/dist/features/tester/seed-data/authored/community-support__support-tickets.js +4 -4
- package/dist/features/tester/seed-data/authored/content-discovery__blog.js +19 -2
- package/dist/features/tester/seed-data/authored/content-discovery__category-brand-relations.js +4 -4
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +5 -5
- package/dist/features/tester/seed-data/authored/content-discovery__coupons.js +41 -6
- package/dist/features/tester/seed-data/authored/content-discovery__event-detail-subroutes.js +63 -8
- package/dist/features/tester/seed-data/authored/content-discovery__event-participation.js +29 -29
- package/dist/features/tester/seed-data/authored/content-discovery__events.js +2 -2
- package/dist/features/tester/seed-data/authored/content-discovery__faq-help.js +23 -1
- package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +6 -6
- package/dist/features/tester/seed-data/authored/content-discovery__search.js +1 -1
- package/dist/features/tester/seed-data/authored/content-discovery__store-reviews-aggregate.js +1 -1
- package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.js +31 -6
- package/dist/features/tester/seed-data/authored/cta-layout__dialog-footers.js +34 -10
- package/dist/features/tester/seed-data/authored/cta-layout__editor-action-bar.js +35 -8
- package/dist/features/tester/seed-data/authored/cta-layout__navbar-ctas.js +31 -9
- package/dist/features/tester/seed-data/authored/cta-layout__product-bottom-bar.js +29 -3
- package/dist/features/tester/seed-data/authored/design-ux__back-to-top-button.js +1 -1
- package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.js +10 -10
- package/dist/features/tester/seed-data/authored/design-ux__dashboard-layout.js +3 -3
- package/dist/features/tester/seed-data/authored/design-ux__footer-theme.js +2 -2
- package/dist/features/tester/seed-data/authored/design-ux__form-validation-errors.js +2 -2
- package/dist/features/tester/seed-data/authored/design-ux__general-design.js +4 -4
- package/dist/features/tester/seed-data/authored/design-ux__hand-mode-layout.js +1 -1
- package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.js +8 -8
- package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +2 -2
- package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +2 -2
- package/dist/features/tester/seed-data/authored/page-wiring__data-loss.js +39 -16
- package/dist/features/tester/seed-data/authored/page-wiring__detail-pages.js +43 -8
- package/dist/features/tester/seed-data/authored/page-wiring__drawer-pages.js +40 -14
- package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +31 -14
- package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +7 -6
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +14 -5
- package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +3 -3
- package/dist/features/tester/seed-data/authored/public-pages__help-how-it-works.js +1 -1
- package/dist/features/tester/seed-data/authored/public-pages__help-scams-guides-subpages.js +4 -2
- package/dist/features/tester/seed-data/authored/public-pages__legal-policy-pages.js +3 -3
- package/dist/features/tester/seed-data/authored/public-pages__newly-wired-browse-indexes.js +5 -5
- package/dist/features/tester/seed-data/authored/public-pages__store-tabs.js +5 -5
- package/dist/features/tester/seed-data/authored/public-pages__stores-sellers-directories.js +3 -3
- package/dist/features/tester/seed-data/authored/search-and-nav__employee-permissions.js +45 -11
- package/dist/features/tester/seed-data/authored/search-and-nav__header-search.js +57 -17
- package/dist/features/tester/seed-data/authored/search-and-nav__settings-deep-links.js +36 -5
- package/dist/features/tester/seed-data/authored/search-and-nav__sidebar-search.js +29 -4
- package/dist/features/tester/seed-data/authored/selling__become-seller.js +23 -0
- package/dist/features/tester/seed-data/authored/selling__final-sale-authoring.js +14 -13
- package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +14 -9
- package/dist/features/tester/seed-data/authored/selling__listing-edit-roundtrip.js +34 -7
- package/dist/features/tester/seed-data/authored/selling__listing-lifecycle.js +17 -7
- package/dist/features/tester/seed-data/authored/selling__listing-type-fields-roundtrip.js +9 -9
- package/dist/features/tester/seed-data/authored/selling__media-limits.js +9 -4
- package/dist/features/tester/seed-data/authored/selling__product-upload-details.js +3 -3
- package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.js +3 -3
- package/dist/features/tester/seed-data/authored/selling__sectionised-forms.js +25 -1
- package/dist/features/tester/seed-data/authored/selling__seller-analytics-payouts.js +27 -6
- package/dist/features/tester/seed-data/authored/selling__seller-bids-bundles-filters.js +19 -1
- package/dist/features/tester/seed-data/authored/selling__seller-catalog-org.js +63 -22
- package/dist/features/tester/seed-data/authored/selling__seller-custom-brands.js +9 -5
- package/dist/features/tester/seed-data/authored/selling__seller-guide.js +7 -6
- package/dist/features/tester/seed-data/authored/selling__seller-listing-types.js +3 -2
- package/dist/features/tester/seed-data/authored/selling__seller-marketing-extras.js +18 -18
- package/dist/features/tester/seed-data/authored/selling__seller-ops-comms.js +13 -12
- package/dist/features/tester/seed-data/authored/selling__seller-orders-at-scale.js +19 -3
- package/dist/features/tester/seed-data/authored/selling__seller-orders.js +22 -4
- package/dist/features/tester/seed-data/authored/selling__seller-shipping-payouts-setup.js +20 -8
- package/dist/features/tester/seed-data/authored/selling__store-dashboard-navigation.js +4 -4
- package/dist/features/tester/seed-data/authored/selling__store-uncovered-pages.js +2 -2
- package/dist/features/tester/seed-data/authored/seo__canonical-and-host.js +5 -5
- package/dist/features/tester/seed-data/authored/seo__og-images.js +1 -1
- package/dist/features/tester/seed-data/authored/seo__page-metadata.js +7 -7
- package/dist/features/tester/seed-data/authored/seo__sitemap-and-robots.js +3 -3
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +123 -21
- package/dist/features/tester/utils/phases.d.ts +17 -0
- package/dist/features/tester/utils/phases.js +29 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/seo/json-ld.d.ts +23 -0
- package/dist/seo/json-ld.js +3 -2
- package/dist/server.d.ts +1 -0
- package/dist/server.js +2 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/PaginatedSelect.js +0 -0
- package/package.json +1 -1
|
@@ -81,15 +81,37 @@ export interface Offer {
|
|
|
81
81
|
}
|
|
82
82
|
export interface OfferAdapterOptions {
|
|
83
83
|
/**
|
|
84
|
-
*
|
|
84
|
+
* What to do with `buyerUid`/`buyerName`/`buyerEmail`.
|
|
85
85
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
86
|
+
* | value | emits |
|
|
87
|
+
* |---|---|
|
|
88
|
+
* | `"omit"` (default) | nothing — the three keys are absent |
|
|
89
|
+
* | `"masked"` | `buyerName` `"M*** U***"`, `buyerEmail` `"m***@***.in"`, no uid |
|
|
90
|
+
* | `"full"` | verbatim |
|
|
91
|
+
*
|
|
92
|
+
* 🛑 **This replaced a boolean, because the boolean had no way to say
|
|
93
|
+
* "masked" and the seller route needed exactly that.** It worked around the
|
|
94
|
+
* gap by calling `maskOfferForSeller(o)` and then adapting with the flag
|
|
95
|
+
* OFF — so the mask ran, produced a masked name, and the adapter deleted it
|
|
96
|
+
* on the very next line. Every one of the 13 rows on `/store/offers` read
|
|
97
|
+
* **"Unknown buyer"**, measured live as tyson@beybladearena.in, and so did
|
|
98
|
+
* the detail modal.
|
|
99
|
+
*
|
|
100
|
+
* That is worse than cosmetic: a seller could not tell two offers on the same
|
|
101
|
+
* listing apart, which is the one thing they need when two people are bidding
|
|
102
|
+
* up the same item. It also silently disagreed with the sibling server action
|
|
103
|
+
* `listSellerOffers`, which masks and returns the name — two seller read
|
|
104
|
+
* paths, two answers (Root Cause #38), and a mask whose output was discarded
|
|
105
|
+
* (Root Cause #50).
|
|
106
|
+
*
|
|
107
|
+
* Masking now happens INSIDE the adapter, so a caller cannot apply one half.
|
|
108
|
+
* `"masked"` deliberately omits `buyerUid` as well: a uid is not a display
|
|
109
|
+
* value and there is nothing to partially reveal about it.
|
|
110
|
+
*
|
|
111
|
+
* The default stays the safe one — a caller that says nothing under-shares
|
|
112
|
+
* rather than leaks, which is why the old boolean defaulted false too.
|
|
91
113
|
*/
|
|
92
|
-
|
|
114
|
+
buyerIdentity?: "omit" | "masked" | "full";
|
|
93
115
|
}
|
|
94
116
|
export declare function offerDocumentToOffer(doc: OfferDocument, opts?: OfferAdapterOptions): Offer;
|
|
95
117
|
/**
|
|
@@ -10,12 +10,25 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Mirrors `orders/adapters.ts`, including its `toIsoOrUndefined` helper.
|
|
12
12
|
*/
|
|
13
|
+
// Display masking only — `pii-mask.ts` is the crypto-FREE half that Root Cause
|
|
14
|
+
// #24 split out of `pii-encrypt.ts` precisely so it could be imported freely.
|
|
15
|
+
import { maskEmail, maskName } from "../../../../security/pii-mask";
|
|
13
16
|
function toIsoOrUndefined(value) {
|
|
14
17
|
return value instanceof Date ? value.toISOString() : undefined;
|
|
15
18
|
}
|
|
16
19
|
function toIso(value) {
|
|
17
20
|
return value instanceof Date ? value.toISOString() : String(value);
|
|
18
21
|
}
|
|
22
|
+
/** The three buyer keys, resolved per `buyerIdentity`. Absent keys stay absent. */
|
|
23
|
+
function buyerIdentityFields(doc, mode) {
|
|
24
|
+
if (mode === "full") {
|
|
25
|
+
return { buyerUid: doc.buyerUid, buyerName: doc.buyerName, buyerEmail: doc.buyerEmail };
|
|
26
|
+
}
|
|
27
|
+
if (mode === "masked") {
|
|
28
|
+
return { buyerName: maskName(doc.buyerName), buyerEmail: maskEmail(doc.buyerEmail) };
|
|
29
|
+
}
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
19
32
|
export function offerDocumentToOffer(doc, opts = {}) {
|
|
20
33
|
const superseded = doc.status === "withdrawn" && Boolean(doc.supersededByOfferId);
|
|
21
34
|
return {
|
|
@@ -59,9 +72,7 @@ export function offerDocumentToOffer(doc, opts = {}) {
|
|
|
59
72
|
superseded,
|
|
60
73
|
cancelledByAdminUid: doc.cancelledByAdminUid,
|
|
61
74
|
cancelReason: doc.cancelReason,
|
|
62
|
-
...(opts.
|
|
63
|
-
? { buyerUid: doc.buyerUid, buyerName: doc.buyerName, buyerEmail: doc.buyerEmail }
|
|
64
|
-
: {}),
|
|
75
|
+
...buyerIdentityFields(doc, opts.buyerIdentity),
|
|
65
76
|
};
|
|
66
77
|
}
|
|
67
78
|
/**
|
|
@@ -1,5 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🛑 A date that is not a `Date` INSTANCE used to be silently dropped, and that
|
|
3
|
+
* is not a hypothetical — it blanked the buyer's tracking page.
|
|
4
|
+
*
|
|
5
|
+
* The old body was `value instanceof Date ? value.toISOString() : undefined`,
|
|
6
|
+
* and its parameter was typed `Date | undefined`, so TypeScript had no reason to
|
|
7
|
+
* object: `OrderDocument` declares these fields as `Date`. What Firestore hands
|
|
8
|
+
* back does not always agree. Measured end to end after a seller marked an order
|
|
9
|
+
* shipped: the stored document carried
|
|
10
|
+
* `shippingDate: "2026-09-29T09:06:22.381Z"` — an ISO **string**, because that is
|
|
11
|
+
* what the ship write path stores — while `orderDate`, written at creation, was a
|
|
12
|
+
* real `Date`. So the adapter mapped `orderDate` and dropped `shippingDate`, and
|
|
13
|
+
* `/user/orders/[id]/track` rendered the Shipped step with an em-dash placeholder
|
|
14
|
+
* beside a perfectly correct carrier and tracking number. Root Cause #57's shape:
|
|
15
|
+
* one field lost in an adapter, presenting as a UI that looks merely incomplete.
|
|
16
|
+
*
|
|
17
|
+
* Three shapes are accepted because all three genuinely occur here: a `Date`, a
|
|
18
|
+
* Firestore `Timestamp` (duck-typed on `toDate` rather than imported, so this
|
|
19
|
+
* file stays free of a firebase-admin import — appkit Export Rules), and an ISO
|
|
20
|
+
* string. Anything else, including an unparseable string, still yields
|
|
21
|
+
* `undefined` — a wrong date is worse than a missing one, and the timeline is
|
|
22
|
+
* built to render the em-dash honestly.
|
|
23
|
+
*/
|
|
1
24
|
function toIsoOrUndefined(value) {
|
|
2
|
-
|
|
25
|
+
if (!value)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (value instanceof Date) {
|
|
28
|
+
return Number.isNaN(value.getTime()) ? undefined : value.toISOString();
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === "object" && typeof value.toDate === "function") {
|
|
31
|
+
const d = value.toDate();
|
|
32
|
+
return d instanceof Date && !Number.isNaN(d.getTime()) ? d.toISOString() : undefined;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
35
|
+
const d = new Date(value);
|
|
36
|
+
return Number.isNaN(d.getTime()) ? undefined : d.toISOString();
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
3
39
|
}
|
|
4
40
|
export function orderDocumentToOrder(doc) {
|
|
5
41
|
const items = doc.items?.length
|
|
@@ -107,6 +143,23 @@ export function orderDocumentToOrder(doc) {
|
|
|
107
143
|
shipmentProtectionAddon: doc.shipmentProtectionAddon,
|
|
108
144
|
shipmentProtectionFee: doc.shipmentProtectionFee,
|
|
109
145
|
platformFee: doc.platformFee,
|
|
146
|
+
/*
|
|
147
|
+
* 🛑 `gstAmount` -> `tax`, and codHandlingFee alongside it.
|
|
148
|
+
*
|
|
149
|
+
* The comment four lines up is about exactly this and the same block still
|
|
150
|
+
* dropped two fees. The invoice renders its GST line as
|
|
151
|
+
* `order.tax !== undefined && order.tax > 0`, and nothing ever set `tax`,
|
|
152
|
+
* so that line could never appear — Root Cause #57's silent "not
|
|
153
|
+
* applicable" fallback, on a financial document.
|
|
154
|
+
*
|
|
155
|
+
* Measured on a real order: the invoice listed Subtotal ₹899.00 and
|
|
156
|
+
* Shipping ₹77.00 against a stated Total of ₹997.80, leaving ₹21.80 with
|
|
157
|
+
* nothing to explain it (₹10 platform fee + ₹10 WhatsApp updates +
|
|
158
|
+
* ₹1.80 GST). An invoice whose lines do not sum to its own total is a
|
|
159
|
+
* GST-compliance problem, not a cosmetic one.
|
|
160
|
+
*/
|
|
161
|
+
tax: doc.gstAmount,
|
|
162
|
+
codHandlingFee: doc.codHandlingFee,
|
|
110
163
|
trackingNumber: doc.trackingNumber,
|
|
111
164
|
shippingCarrier: doc.shippingCarrier,
|
|
112
165
|
trackingUrl: doc.trackingUrl,
|
package/dist/client.d.ts
CHANGED
|
@@ -462,6 +462,7 @@ export { BecomeSellerView } from "./features/account/index";
|
|
|
462
462
|
export type { BidDocument } from "./features/auctions/index";
|
|
463
463
|
export type { ClaimedCouponDocument } from "./features/promotions/schemas";
|
|
464
464
|
export { groupOrderItemsByLine } from "./features/orders/index";
|
|
465
|
+
export { shortOrderRef } from "./features/orders/index";
|
|
465
466
|
export type { LineOrderGroup, OutOfStockPolicy } from "./features/orders/index";
|
|
466
467
|
export { MediaUploadField } from "./features/media/index";
|
|
467
468
|
export { PrizeRevealModal } from "./features/products/components/PrizeRevealModal";
|
package/dist/client.js
CHANGED
|
@@ -467,6 +467,7 @@ export { Card, CardBody } from "./ui/index";
|
|
|
467
467
|
export { SellerCouponsView, SellerCreateProductView, SellerEditProductView, useBecomeSeller, } from "./features/seller/index";
|
|
468
468
|
export { BecomeSellerView } from "./features/account/index";
|
|
469
469
|
export { groupOrderItemsByLine } from "./features/orders/index";
|
|
470
|
+
export { shortOrderRef } from "./features/orders/index";
|
|
470
471
|
export { MediaUploadField } from "./features/media/index";
|
|
471
472
|
export { PrizeRevealModal } from "./features/products/components/PrizeRevealModal";
|
|
472
473
|
export { useProduct } from "./features/products/index";
|
|
@@ -9,6 +9,7 @@ import { PaginatedSelect } from "../../../ui/components/PaginatedSelect";
|
|
|
9
9
|
import { DataListingView } from "../../admin/components/DataListingView";
|
|
10
10
|
import { OrderCard } from "../../orders/components/OrdersList";
|
|
11
11
|
import { useOrderScope } from "../../orders/components/OrderScopeTabs";
|
|
12
|
+
import { useSearchParams } from "next/navigation";
|
|
12
13
|
const CANCELLABLE_STATUSES = new Set(["pending", "confirmed", "processing"]);
|
|
13
14
|
const TRACKABLE_STATUSES = new Set(["shipped"]);
|
|
14
15
|
const SORT_OPTIONS = [
|
|
@@ -45,6 +46,33 @@ const STATUS_OPTIONS = [
|
|
|
45
46
|
];
|
|
46
47
|
export function UserOrdersView({ onOrderClick }) {
|
|
47
48
|
const orderScope = useOrderScope();
|
|
49
|
+
/*
|
|
50
|
+
* 🛑 A FILTERED empty result is not an empty account, and saying so is a lie
|
|
51
|
+
* the buyer has no way to check.
|
|
52
|
+
*
|
|
53
|
+
* Both empty states here read "You haven't placed any orders yet." That is
|
|
54
|
+
* correct for a new account and flatly wrong the moment a search or a status
|
|
55
|
+
* chip is what emptied the list. Measured: /user/orders?q=zzzznope on an
|
|
56
|
+
* account with roughly thirty real orders rendered exactly that sentence — a
|
|
57
|
+
* buyer searching for an order they could not find would reasonably conclude
|
|
58
|
+
* their whole history had gone.
|
|
59
|
+
*
|
|
60
|
+
* Same shape as the poll leaderboard fixed earlier in this run, where a
|
|
61
|
+
* withheld tally rendered as "No votes yet." beside a participant count of
|
|
62
|
+
* 365: an empty result must not be reported as an absolute zero when a filter
|
|
63
|
+
* is the reason.
|
|
64
|
+
*
|
|
65
|
+
* Read from the URL rather than threaded through `renderCards`, whose
|
|
66
|
+
* signature is `(rows, view, selection, isLoading)` and carries no filter
|
|
67
|
+
* state. `useUrlTable`/`useSearchParams` hold no local state, so a second
|
|
68
|
+
* reader against the same URL stays in sync with the listing's own
|
|
69
|
+
* (CLAUDE.md, Root Cause #35).
|
|
70
|
+
*/
|
|
71
|
+
const searchParams = useSearchParams();
|
|
72
|
+
const hasNarrowingFilter = ["q", "search", "status", "orderType", "filters"].some((k) => Boolean(searchParams.get(k)));
|
|
73
|
+
const emptyMessage = hasNarrowingFilter
|
|
74
|
+
? "No orders match your search or filters."
|
|
75
|
+
: "You haven't placed any orders yet.";
|
|
48
76
|
const config = {
|
|
49
77
|
portal: "user",
|
|
50
78
|
title: "My Orders",
|
|
@@ -56,7 +84,7 @@ export function UserOrdersView({ onOrderClick }) {
|
|
|
56
84
|
// match more.
|
|
57
85
|
fields: ["id"],
|
|
58
86
|
},
|
|
59
|
-
emptyLabel:
|
|
87
|
+
emptyLabel: emptyMessage,
|
|
60
88
|
filterKeys: ["status", "orderType"],
|
|
61
89
|
defaultSort: sortBy("createdAt", "DESC"),
|
|
62
90
|
queryKey: ["user", "orders", "listing"],
|
|
@@ -82,7 +110,7 @@ export function UserOrdersView({ onOrderClick }) {
|
|
|
82
110
|
return (_jsx(Stack, { gap: "md", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Div, { className: "h-24 animate-pulse border border-[var(--appkit-color-border)]", rounded: "xl" }, i))) }));
|
|
83
111
|
}
|
|
84
112
|
if (rows.length === 0) {
|
|
85
|
-
return _jsx(Text, { color: "muted", children:
|
|
113
|
+
return _jsx(Text, { color: "muted", children: emptyMessage });
|
|
86
114
|
}
|
|
87
115
|
return (_jsx(Grid, { gap: "md", className: "grid-cols-1", children: rows.map((order) => (_jsx(OrderCard, { order: order, onClick: onOrderClick, renderActions: () => (_jsxs(_Fragment, { children: [_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(TextLink, { href: String(ROUTES.USER.ORDER_DETAIL(order.id)), children: ACTIONS.USER["view-order"].label }) }), TRACKABLE_STATUSES.has(order.orderStatus) && (_jsx(Button, { asChild: true, variant: "ghost", size: "sm", children: _jsx(TextLink, { href: String(ROUTES.USER.ORDER_TRACK(order.id)), children: ACTIONS.USER["track-order"].label }) })), CANCELLABLE_STATUSES.has(order.orderStatus) && (_jsx(Button, { asChild: true, variant: "ghost", size: "sm", children: _jsx(TextLink, { href: String(ROUTES.USER.ORDER_CANCEL(order.id)), children: ACTIONS.USER["cancel-order"].label }) }))] })) }, order.id))) }));
|
|
88
116
|
},
|
|
@@ -64,13 +64,28 @@ export declare function useCreateAddress(options?: {
|
|
|
64
64
|
/**
|
|
65
65
|
* Update options.
|
|
66
66
|
*
|
|
67
|
-
* `method`
|
|
68
|
-
* exports PATCH and `/api/store/addresses/[id]` exports PUT, and
|
|
69
|
-
* `SellerAddressesView` hardcodes PUT against the latter. Changing the route's
|
|
70
|
-
* verb would break that working call site, so the hook bends instead.
|
|
67
|
+
* 🛑 `method` DEFAULTS TO PUT, because both portals take PUT.
|
|
71
68
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
69
|
+
* This defaulted to PATCH on the strength of a comment saying "the two portals
|
|
70
|
+
* disagree: `/api/user/addresses/[id]` exports PATCH and
|
|
71
|
+
* `/api/store/addresses/[id]` exports PUT". Measured against both route files,
|
|
72
|
+
* each exports **GET / PUT / DELETE** and NEITHER exports PATCH — so the
|
|
73
|
+
* premise was stale and the default was the only thing disagreeing with either
|
|
74
|
+
* of them. Every buyer edit sent `PATCH /api/user/addresses/{id}` and took a
|
|
75
|
+
* **405**, surfacing as `ApiClientError: Invalid JSON response`. The form stays
|
|
76
|
+
* on the edit page and the record is unchanged. (`EditAddressClient` does call
|
|
77
|
+
* `showToast("Failed to update address.", "error")` from `onError`; whether the
|
|
78
|
+
* buyer actually sees it was NOT established — every measurement landed after a
|
|
79
|
+
* toast's lifetime, so it is not claimed either way here.)
|
|
80
|
+
*
|
|
81
|
+
* The prop is kept rather than deleted so a portal that genuinely wants PATCH
|
|
82
|
+
* can ask for it, and `SellerAddressesView` keeps passing PUT explicitly.
|
|
83
|
+
*
|
|
84
|
+
* 🛑 That comment also cited `audit-client-verb-match` as proof this could
|
|
85
|
+
* not happen. The audit reports **clean** while the 405 is live: it resolves
|
|
86
|
+
* literal `apiClient.patch("/some/path")` calls, and cannot see a verb chosen
|
|
87
|
+
* at runtime by `method === "PUT" ? apiClient.put(...) : apiClient.patch(...)`.
|
|
88
|
+
* A gate you have not watched fail is not a gate (Root Cause #87).
|
|
74
89
|
*/
|
|
75
90
|
export interface UpdateAddressOptions {
|
|
76
91
|
byIdEndpoint?: (id: string) => string;
|
|
@@ -46,7 +46,7 @@ export function useCreateAddress(options) {
|
|
|
46
46
|
export function useUpdateAddress(id, options) {
|
|
47
47
|
const queryClient = useQueryClient();
|
|
48
48
|
const byIdEndpoint = options?.byIdEndpoint ?? DEFAULT_ENDPOINTS.byId;
|
|
49
|
-
const method = options?.method ?? "
|
|
49
|
+
const method = options?.method ?? "PUT";
|
|
50
50
|
return useMutation({
|
|
51
51
|
mutationFn: (data) => method === "PUT"
|
|
52
52
|
? apiClient.put(byIdEndpoint(id), data)
|
|
@@ -45,7 +45,64 @@ import * as React from "react";
|
|
|
45
45
|
import { normalizeError } from "../../../errors/normalize";
|
|
46
46
|
import { countryFor, isValidPostalCode } from "../../../constants/geo/countries";
|
|
47
47
|
const ENDPOINT = "https://api.zippopotam.us";
|
|
48
|
+
const INDIA_ENDPOINT = "https://api.postalpincode.in/pincode";
|
|
48
49
|
const DEBOUNCE_MS = 400;
|
|
50
|
+
/**
|
|
51
|
+
* 🛑 India does not go through Zippopotam, because Zippopotam has no city
|
|
52
|
+
* for an Indian PIN.
|
|
53
|
+
*
|
|
54
|
+
* Its `places[].place name` is the POST OFFICE, and measured against the five
|
|
55
|
+
* largest Indian cities not one of them returned the city:
|
|
56
|
+
*
|
|
57
|
+
* 560001 Bengaluru — "Rajbhavan"
|
|
58
|
+
* 400001 Mumbai — "Haji S Musafarkhana"
|
|
59
|
+
* 110001 Delhi — "Janpath"
|
|
60
|
+
* 700001 Kolkata — "Lalbazar"
|
|
61
|
+
* 452001 Indore — "Indore Jail Road"
|
|
62
|
+
*
|
|
63
|
+
* So the City field was filled with a street or locality on every Indian
|
|
64
|
+
* address, which is worse than leaving it blank: the buyer sees a
|
|
65
|
+
* plausible-looking filled field and has no reason to correct it, and the
|
|
66
|
+
* courier gets an address with no city. This hook's own promise — "typing a
|
|
67
|
+
* zipcode should fill in the city" — was never once kept in the only market
|
|
68
|
+
* this site serves.
|
|
69
|
+
*
|
|
70
|
+
* India Post publishes `District`, which IS the city (Bangalore, Mumbai,
|
|
71
|
+
* Indore). Note it uses the older spellings; that is the authority's own data
|
|
72
|
+
* and not ours to rewrite.
|
|
73
|
+
*/
|
|
74
|
+
async function lookupIndia(code, signal) {
|
|
75
|
+
const res = await fetch(`${INDIA_ENDPOINT}/${encodeURIComponent(code)}`, { signal });
|
|
76
|
+
if (!res.ok)
|
|
77
|
+
return null;
|
|
78
|
+
const json = (await res.json());
|
|
79
|
+
const row = json?.[0];
|
|
80
|
+
if (row?.Status !== "Success")
|
|
81
|
+
return null;
|
|
82
|
+
const offices = (row.PostOffice ?? []).filter(Boolean);
|
|
83
|
+
if (offices.length === 0)
|
|
84
|
+
return null;
|
|
85
|
+
/*
|
|
86
|
+
* The MOST COMMON district, not the first. A PIN on a boundary returns post
|
|
87
|
+
* offices from two districts — 400001 lists 'Mumbai' and 'Raigarh(MH)' —
|
|
88
|
+
* and the first row is not reliably the one the buyer means.
|
|
89
|
+
*/
|
|
90
|
+
const tally = new Map();
|
|
91
|
+
for (const o of offices) {
|
|
92
|
+
const d = (o.District ?? "").trim();
|
|
93
|
+
if (d)
|
|
94
|
+
tally.set(d, (tally.get(d) ?? 0) + 1);
|
|
95
|
+
}
|
|
96
|
+
let city = "";
|
|
97
|
+
let best = 0;
|
|
98
|
+
for (const [d, n] of tally) {
|
|
99
|
+
if (n > best) {
|
|
100
|
+
city = d;
|
|
101
|
+
best = n;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { city, state: (offices[0].State ?? "").trim() };
|
|
105
|
+
}
|
|
49
106
|
/**
|
|
50
107
|
* Per-session cache, keyed `{country}:{postal}`.
|
|
51
108
|
*
|
|
@@ -94,18 +151,33 @@ export function usePostalLookup({ onResolved, enabled = true, }) {
|
|
|
94
151
|
const controller = new AbortController();
|
|
95
152
|
abortRef.current = controller;
|
|
96
153
|
setIsLooking(true);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
154
|
+
/*
|
|
155
|
+
* India has its own source — see `lookupIndia`. Everywhere else keeps
|
|
156
|
+
* Zippopotam, whose "place name" IS the city outside India.
|
|
157
|
+
*/
|
|
158
|
+
const request = def.code.toUpperCase() === "IN"
|
|
159
|
+
? lookupIndia(code, controller.signal)
|
|
160
|
+
: fetch(`${ENDPOINT}/${def.code.toLowerCase()}/${encodeURIComponent(code)}`, {
|
|
161
|
+
signal: controller.signal,
|
|
162
|
+
})
|
|
163
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
164
|
+
.then((json) => {
|
|
165
|
+
const place = json?.places?.[0];
|
|
166
|
+
return place
|
|
167
|
+
? {
|
|
168
|
+
city: place["place name"] ?? "",
|
|
169
|
+
state: place.state ?? place["state abbreviation"] ?? "",
|
|
170
|
+
}
|
|
171
|
+
: null;
|
|
172
|
+
});
|
|
173
|
+
void request
|
|
174
|
+
.then((resolved) => {
|
|
175
|
+
/*
|
|
176
|
+
* A result with neither field is a miss, not a hit — handing the
|
|
177
|
+
* form two empty strings would clear nothing and resolve nothing,
|
|
178
|
+
* while caching it as success hides a real answer for the session.
|
|
179
|
+
*/
|
|
180
|
+
const result = resolved && (resolved.city || resolved.state) ? resolved : null;
|
|
109
181
|
CACHE.set(key, result);
|
|
110
182
|
if (result)
|
|
111
183
|
onResolvedRef.current(result);
|
|
@@ -65,10 +65,45 @@ const ADMIN_CATEGORIES_CONFIG = {
|
|
|
65
65
|
columns: COLUMNS,
|
|
66
66
|
mapRows: (response) => {
|
|
67
67
|
const sourceItems = extractCategoryRows(response);
|
|
68
|
-
|
|
68
|
+
const rows = toRecordArray(sourceItems);
|
|
69
|
+
/*
|
|
70
|
+
* 🛑 THE PARENT COLUMN READ A FIELD THAT DOES NOT EXIST.
|
|
71
|
+
*
|
|
72
|
+
* It was `toStringValue(item.parentId, "root")`, and `CategoryDocument` has
|
|
73
|
+
* no `parentId` — the field is **`parentIds: string[]`**, the full ancestor
|
|
74
|
+
* chain. So the fallback fired on every row: measured on the live list, all
|
|
75
|
+
* **100** categories displayed "Parent: root" while their tiers ranged 0-4
|
|
76
|
+
* and only 22 were actually roots. The tier beside it renders correctly from
|
|
77
|
+
* the same document, which is what makes the wrong value so easy to trust.
|
|
78
|
+
*
|
|
79
|
+
* CLAUDE.md flags this exact confusion in the Seed Data Reference ("NOT a
|
|
80
|
+
* single `parentId`"), and `parentId` does still exist — as an optional
|
|
81
|
+
* field on a create/update INPUT shape, which is why it typechecks here and
|
|
82
|
+
* is undefined at runtime.
|
|
83
|
+
*
|
|
84
|
+
* `parentIds` is ordered ancestors-nearest-LAST ("matching how parentIds is
|
|
85
|
+
* read elsewhere", category-tree.ts), so the immediate parent is the final
|
|
86
|
+
* element. The name is shown where the list already knows it: every
|
|
87
|
+
* category is in this same response, so the lookup costs nothing and an id
|
|
88
|
+
* is a poor answer to "what is this filed under".
|
|
89
|
+
*/
|
|
90
|
+
const nameById = new Map();
|
|
91
|
+
for (const r of rows) {
|
|
92
|
+
const id = toStringValue(r.id, "");
|
|
93
|
+
if (id)
|
|
94
|
+
nameById.set(id, toStringValue(r.name, id));
|
|
95
|
+
}
|
|
96
|
+
const parentLabel = (item) => {
|
|
97
|
+
const chain = Array.isArray(item.parentIds) ? item.parentIds : [];
|
|
98
|
+
const immediate = chain.length ? String(chain[chain.length - 1]) : "";
|
|
99
|
+
if (!immediate)
|
|
100
|
+
return "root";
|
|
101
|
+
return nameById.get(immediate) ?? immediate;
|
|
102
|
+
};
|
|
103
|
+
return rows.map((item, index) => ({
|
|
69
104
|
id: toStringValue(item.id, `category-${index}`),
|
|
70
105
|
primary: toStringValue(item.name, "Untitled category"),
|
|
71
|
-
secondary: `Slug: ${toStringValue(item.slug, "no-slug")} · Tier: ${typeof item.tier === "number" ? String(item.tier) : "-"} · Parent: ${
|
|
106
|
+
secondary: `Slug: ${toStringValue(item.slug, "no-slug")} · Tier: ${typeof item.tier === "number" ? String(item.tier) : "-"} · Parent: ${parentLabel(item)}`,
|
|
72
107
|
status: typeof item.isActive === "boolean"
|
|
73
108
|
? item.isActive
|
|
74
109
|
? "Active"
|
|
@@ -36,7 +36,7 @@ import React, { useState } from "react";
|
|
|
36
36
|
import { useQueryClient } from "@tanstack/react-query";
|
|
37
37
|
import { FilterChipGroup, ListingLayout, RecordDetailModal, RowActionMenu, useToast, } from "../../../ui";
|
|
38
38
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
39
|
-
import { ADMIN_OFFER_STATUS_TABS } from "../constants/filter-tabs";
|
|
39
|
+
import { ADMIN_OFFER_STATUS_TABS, offerStatusLabel } from "../constants/filter-tabs";
|
|
40
40
|
import { OfferPhaseTimeline } from "../../seller/components/OfferPhaseTimeline";
|
|
41
41
|
import { QuickFormDrawer } from "../../shell/QuickFormDrawer";
|
|
42
42
|
import { cancelOfferFormSchema } from "../../seller/schemas/offer-forms";
|
|
@@ -136,7 +136,7 @@ export function AdminOffersView({ children, ...props }) {
|
|
|
136
136
|
`Listed: ${toCurrency(item.listedPrice)}`,
|
|
137
137
|
`${toStringValue(item.buyerName, "Unknown buyer")} → ${toStringValue(item.storeName, "Unknown store")}`,
|
|
138
138
|
].join(" · "),
|
|
139
|
-
status: toStringValue(item.status, "pending"),
|
|
139
|
+
status: offerStatusLabel(toStringValue(item.status, "pending"), "admin"),
|
|
140
140
|
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
141
141
|
detail: item,
|
|
142
142
|
})),
|
|
@@ -650,6 +650,31 @@ export declare const SELLER_OFFER_STATUS_TABS: readonly [{
|
|
|
650
650
|
readonly id: "paid";
|
|
651
651
|
readonly label: "Paid";
|
|
652
652
|
}];
|
|
653
|
+
/**
|
|
654
|
+
* The display label for an offer status — the SAME words as that audience's
|
|
655
|
+
* own filter chips.
|
|
656
|
+
*
|
|
657
|
+
* 🛑 Both offer views rendered the raw stored value as the badge, so a seller
|
|
658
|
+
* filtered by a chip reading **"Rejected"** and the matching rows came back
|
|
659
|
+
* badged **"declined"** — two words for one state, on one screen. Every other
|
|
660
|
+
* badge was lowercase too ("pending", "accepted") while the fallback beside it
|
|
661
|
+
* was capitalised `"Pending"`, which is the tell that display-cased values were
|
|
662
|
+
* always the intent.
|
|
663
|
+
*
|
|
664
|
+
* 🛑 **`audience` is not ceremony — the two chip sets DISAGREE on purpose.**
|
|
665
|
+
* `declined` is "Rejected" to a seller (see the header above: kept for
|
|
666
|
+
* continuity with existing seller-facing wording) and "Declined" to an admin.
|
|
667
|
+
* A single shared lookup would therefore have printed "Rejected" on the admin
|
|
668
|
+
* screen beside its own "Declined" chip — reintroducing the exact mismatch
|
|
669
|
+
* this function exists to remove, one audience over.
|
|
670
|
+
*
|
|
671
|
+
* Deriving from the arrays rather than writing a third map is the point: a
|
|
672
|
+
* hand-written copy is how ten enumerations of one union came to disagree
|
|
673
|
+
* (Root Cause #61). An unrecognised value falls through to itself, so an
|
|
674
|
+
* eighth status shows up as its raw name rather than vanishing (Root Cause
|
|
675
|
+
* #72 — unknown must never render as absent).
|
|
676
|
+
*/
|
|
677
|
+
export declare function offerStatusLabel(status: string | null | undefined, audience: "seller" | "admin"): string;
|
|
653
678
|
/** Seller > Bids — bid-state filter chip set. Uses the empty-sentinel
|
|
654
679
|
* variant (see `EMPTY_TAB`). */
|
|
655
680
|
export declare const SELLER_BID_STATUS_TABS: readonly [{
|
|
@@ -360,6 +360,36 @@ export const SELLER_OFFER_STATUS_TABS = [
|
|
|
360
360
|
{ id: "withdrawn", label: "Withdrawn" },
|
|
361
361
|
{ id: "paid", label: "Paid" },
|
|
362
362
|
];
|
|
363
|
+
/**
|
|
364
|
+
* The display label for an offer status — the SAME words as that audience's
|
|
365
|
+
* own filter chips.
|
|
366
|
+
*
|
|
367
|
+
* 🛑 Both offer views rendered the raw stored value as the badge, so a seller
|
|
368
|
+
* filtered by a chip reading **"Rejected"** and the matching rows came back
|
|
369
|
+
* badged **"declined"** — two words for one state, on one screen. Every other
|
|
370
|
+
* badge was lowercase too ("pending", "accepted") while the fallback beside it
|
|
371
|
+
* was capitalised `"Pending"`, which is the tell that display-cased values were
|
|
372
|
+
* always the intent.
|
|
373
|
+
*
|
|
374
|
+
* 🛑 **`audience` is not ceremony — the two chip sets DISAGREE on purpose.**
|
|
375
|
+
* `declined` is "Rejected" to a seller (see the header above: kept for
|
|
376
|
+
* continuity with existing seller-facing wording) and "Declined" to an admin.
|
|
377
|
+
* A single shared lookup would therefore have printed "Rejected" on the admin
|
|
378
|
+
* screen beside its own "Declined" chip — reintroducing the exact mismatch
|
|
379
|
+
* this function exists to remove, one audience over.
|
|
380
|
+
*
|
|
381
|
+
* Deriving from the arrays rather than writing a third map is the point: a
|
|
382
|
+
* hand-written copy is how ten enumerations of one union came to disagree
|
|
383
|
+
* (Root Cause #61). An unrecognised value falls through to itself, so an
|
|
384
|
+
* eighth status shows up as its raw name rather than vanishing (Root Cause
|
|
385
|
+
* #72 — unknown must never render as absent).
|
|
386
|
+
*/
|
|
387
|
+
export function offerStatusLabel(status, audience) {
|
|
388
|
+
if (!status)
|
|
389
|
+
return "Pending";
|
|
390
|
+
const tabs = audience === "admin" ? ADMIN_OFFER_STATUS_TABS : SELLER_OFFER_STATUS_TABS;
|
|
391
|
+
return tabs.find((t) => t.id === status)?.label ?? status;
|
|
392
|
+
}
|
|
363
393
|
/** Seller > Bids — bid-state filter chip set. Uses the empty-sentinel
|
|
364
394
|
* variant (see `EMPTY_TAB`). */
|
|
365
395
|
export const SELLER_BID_STATUS_TABS = [
|
|
@@ -41,5 +41,40 @@ export declare function useAdminListingData<TResponse, TRow extends {
|
|
|
41
41
|
export declare function toRecordArray(value: unknown): ListingItemRecord[];
|
|
42
42
|
export declare function toStringValue(value: unknown, fallback?: string): string;
|
|
43
43
|
export declare function toCurrency(value: unknown): string;
|
|
44
|
+
/**
|
|
45
|
+
* A timestamp as human-relative text — **in either direction**.
|
|
46
|
+
*
|
|
47
|
+
* 🛑 **Every FUTURE date used to render as "1m ago".** `deltaMs` is
|
|
48
|
+
* `now - date`, which is negative ahead of now; `deltaMs < hour` then matched,
|
|
49
|
+
* and `Math.max(1, Math.floor(negative))` clamped the result to exactly `1`.
|
|
50
|
+
* So a deadline 48 hours away and one 5 minutes away both read "1m ago", and
|
|
51
|
+
* the clamp is what hid it — without it the string would have been
|
|
52
|
+
* "-2880m ago", which nobody could have mistaken for working.
|
|
53
|
+
*
|
|
54
|
+
* Measured live on `/store/offers` as tyson@beybladearena.in: an offer accepted
|
|
55
|
+
* seconds earlier showed **"Buyer must pay by 1m ago"** against a real
|
|
56
|
+
* `checkoutDeadline` of `now + 48h` (`OFFER_CHECKOUT_WINDOW_MS`). The data was
|
|
57
|
+
* correct and the sentence told the seller — and on the admin view, the admin —
|
|
58
|
+
* that a buyer had already missed a window that had barely opened. On the
|
|
59
|
+
* buyer's own side that reads as "too late to pay for the thing you just
|
|
60
|
+
* negotiated", i.e. an abandoned purchase caused entirely by a formatter.
|
|
61
|
+
*
|
|
62
|
+
* Five call sites render a future-capable field today (offer `expiresAt` and
|
|
63
|
+
* `checkoutDeadline` on both the seller and admin views, and session
|
|
64
|
+
* `expiresAt`), and any of the other 61 becomes one the moment its field can
|
|
65
|
+
* hold a future date — which is why this is fixed here rather than at the call
|
|
66
|
+
* sites.
|
|
67
|
+
*
|
|
68
|
+
* The `Math.max(1, …)` floors stay for the PAST direction: they keep a
|
|
69
|
+
* 40-second-old row reading "1m ago" instead of "0m ago". They are applied to
|
|
70
|
+
* an absolute magnitude now, so they can no longer mask a sign error.
|
|
71
|
+
*
|
|
72
|
+
* NOTE ON ITS HOME: a pure date formatter belongs in `ui/` or
|
|
73
|
+
* `_internal/client/`, not in an admin hook module that 44 files reach into.
|
|
74
|
+
* Moving it means rewriting all 44 imports, so it stays put for now and the
|
|
75
|
+
* seller-side duplicate was deleted instead (see `useSellerListingData.ts`) —
|
|
76
|
+
* that copy had already drifted, returning "just now" for the same future date
|
|
77
|
+
* this one called "1m ago". Two answers to one question is the whole argument.
|
|
78
|
+
*/
|
|
44
79
|
export declare function toRelativeDate(value: unknown): string;
|
|
45
80
|
export {};
|
|
@@ -63,6 +63,41 @@ export function toCurrency(value) {
|
|
|
63
63
|
}
|
|
64
64
|
return formatCurrency(value);
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* A timestamp as human-relative text — **in either direction**.
|
|
68
|
+
*
|
|
69
|
+
* 🛑 **Every FUTURE date used to render as "1m ago".** `deltaMs` is
|
|
70
|
+
* `now - date`, which is negative ahead of now; `deltaMs < hour` then matched,
|
|
71
|
+
* and `Math.max(1, Math.floor(negative))` clamped the result to exactly `1`.
|
|
72
|
+
* So a deadline 48 hours away and one 5 minutes away both read "1m ago", and
|
|
73
|
+
* the clamp is what hid it — without it the string would have been
|
|
74
|
+
* "-2880m ago", which nobody could have mistaken for working.
|
|
75
|
+
*
|
|
76
|
+
* Measured live on `/store/offers` as tyson@beybladearena.in: an offer accepted
|
|
77
|
+
* seconds earlier showed **"Buyer must pay by 1m ago"** against a real
|
|
78
|
+
* `checkoutDeadline` of `now + 48h` (`OFFER_CHECKOUT_WINDOW_MS`). The data was
|
|
79
|
+
* correct and the sentence told the seller — and on the admin view, the admin —
|
|
80
|
+
* that a buyer had already missed a window that had barely opened. On the
|
|
81
|
+
* buyer's own side that reads as "too late to pay for the thing you just
|
|
82
|
+
* negotiated", i.e. an abandoned purchase caused entirely by a formatter.
|
|
83
|
+
*
|
|
84
|
+
* Five call sites render a future-capable field today (offer `expiresAt` and
|
|
85
|
+
* `checkoutDeadline` on both the seller and admin views, and session
|
|
86
|
+
* `expiresAt`), and any of the other 61 becomes one the moment its field can
|
|
87
|
+
* hold a future date — which is why this is fixed here rather than at the call
|
|
88
|
+
* sites.
|
|
89
|
+
*
|
|
90
|
+
* The `Math.max(1, …)` floors stay for the PAST direction: they keep a
|
|
91
|
+
* 40-second-old row reading "1m ago" instead of "0m ago". They are applied to
|
|
92
|
+
* an absolute magnitude now, so they can no longer mask a sign error.
|
|
93
|
+
*
|
|
94
|
+
* NOTE ON ITS HOME: a pure date formatter belongs in `ui/` or
|
|
95
|
+
* `_internal/client/`, not in an admin hook module that 44 files reach into.
|
|
96
|
+
* Moving it means rewriting all 44 imports, so it stays put for now and the
|
|
97
|
+
* seller-side duplicate was deleted instead (see `useSellerListingData.ts`) —
|
|
98
|
+
* that copy had already drifted, returning "just now" for the same future date
|
|
99
|
+
* this one called "1m ago". Two answers to one question is the whole argument.
|
|
100
|
+
*/
|
|
66
101
|
export function toRelativeDate(value) {
|
|
67
102
|
const date = parseDate(value);
|
|
68
103
|
if (!date) {
|
|
@@ -72,17 +107,18 @@ export function toRelativeDate(value) {
|
|
|
72
107
|
const minute = 60000;
|
|
73
108
|
const hour = 60 * minute;
|
|
74
109
|
const day = 24 * hour;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
110
|
+
const future = deltaMs < 0;
|
|
111
|
+
const magnitude = Math.abs(deltaMs);
|
|
112
|
+
// "in 3h" vs "3h ago" — the only difference between the two directions.
|
|
113
|
+
const phrase = (n, unit) => (future ? `in ${n}${unit}` : `${n}${unit} ago`);
|
|
114
|
+
if (magnitude < hour) {
|
|
115
|
+
return phrase(Math.max(1, Math.floor(magnitude / minute)), "m");
|
|
78
116
|
}
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
return `${hours}h ago`;
|
|
117
|
+
if (magnitude < day) {
|
|
118
|
+
return phrase(Math.max(1, Math.floor(magnitude / hour)), "h");
|
|
82
119
|
}
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
return `${days}d ago`;
|
|
120
|
+
if (magnitude < 7 * day) {
|
|
121
|
+
return phrase(Math.max(1, Math.floor(magnitude / day)), "d");
|
|
86
122
|
}
|
|
87
123
|
return date.toLocaleDateString("en-IN", {
|
|
88
124
|
day: "2-digit",
|