Optimization (audited across src/, typecheck green): - Crash guards: PurchaseAuction data?.result + stray text-node, AuctionItemDetail length>0, LiveAuction user?.contractorId, KYC params guard - Hoisted render-defined components: Login MinimalInput (keyboard-focus bug), TenderFullDetail Section/InfoBox (subtree remount) - Memoized hot paths: Spreadsheet ColumnHeaderRow/DataRow, list makeStyles, shared infinite-query select (stops per-render flatMap) - Removed hot-path/in-list console logs; SkeletonList 10->1 shimmer UI/theme: themed AppToast, redesigned sidebar drawer, list empty states, KYC WebView cleanup Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
146 lines
6.2 KiB
TypeScript
146 lines
6.2 KiB
TypeScript
/**
|
|
* Pattern configuration for the unified auction bidding screen/card.
|
|
*
|
|
* Forward and Reverse bidding were 90%-duplicated screens that drifted apart.
|
|
* Every behavioral difference between them is captured HERE, explicitly, so
|
|
* the screen and card contain zero `if (pattern === ...)` logic and a bidding
|
|
* fix can never again land in one pattern but not the other.
|
|
*
|
|
* The stepper is modelled as a bounded number line per pattern:
|
|
* - Forward: bid UP. floor = current bid (never undercut it), no ceiling.
|
|
* - Reverse: bid DOWN. ceiling = current bid (never exceed it), floor = EMD.
|
|
* `stepUp`/`stepDown` return the next value already clamped to [floor, ceil],
|
|
* and `canIncrease`/`canDecrease` are derived from them, so the buttons can
|
|
* never overshoot a bound and can never be "enabled but do nothing".
|
|
*/
|
|
import { FORWARD_AUCTION_ITEM, REVERSE_AUCTION_ITEM } from '../../Services/ApiCall/ApiUrls';
|
|
|
|
export type AuctionPattern = 'Forward' | 'Reverse';
|
|
|
|
export type BiddingFlags = {
|
|
oneTimeBid: boolean;
|
|
rebid: boolean;
|
|
};
|
|
|
|
export type BiddingPatternConfig = {
|
|
pattern: AuctionPattern;
|
|
/** Endpoint returning this pattern's auction items. */
|
|
itemsUrl: string;
|
|
/** Opening amount — the backend uses a different field per pattern. */
|
|
startValue: (item: any) => number;
|
|
/** Stepper step size — always a positive number (see resolveStep). */
|
|
step: (item: any) => number;
|
|
stepLabel: 'Increment' | 'Decrement';
|
|
stepPrefix: '+' | '-';
|
|
/** Lowest value the working bid may reach. */
|
|
floor: (item: any) => number;
|
|
/** Highest value the working bid may reach. */
|
|
ceil: (item: any) => number;
|
|
/** Next value after pressing +/-, already clamped into [floor, ceil]. */
|
|
stepUp: (bidValue: number, item: any) => number;
|
|
stepDown: (bidValue: number, item: any) => number;
|
|
/** True only when the corresponding button would actually move the value. */
|
|
canIncrease: (bidValue: number, item: any) => boolean;
|
|
canDecrease: (bidValue: number, item: any) => boolean;
|
|
/** Gate applied while typing a manual (one-time/rebid) amount. */
|
|
acceptManualInput: (value: number, item: any) => boolean;
|
|
/** Submit-time validation for manual bids; returns an error message or null. */
|
|
validateManualBid: (value: number, item: any) => string | null;
|
|
/** Reverse renders the manual input inline within the stepper row. */
|
|
inlineManualInput: boolean;
|
|
/** Whether the collapsible body opens at full height. */
|
|
isBodyFull: (flags: BiddingFlags, item: any) => boolean;
|
|
/** Whether the Confirm Bid button is rendered. */
|
|
showSubmit: (flags: BiddingFlags, item: any) => boolean;
|
|
};
|
|
|
|
/**
|
|
* Amounts arrive from the backend as strings (e.g. auctionFees: "400"), so any
|
|
* bare `>`/`<`/`+` against them silently string-compares or concatenates. Every
|
|
* numeric read below goes through `num` so steps, bounds and comparisons are
|
|
* always real arithmetic; a missing/non-numeric field degrades to 0.
|
|
*/
|
|
const num = (v: any): number => {
|
|
const n = Number(v);
|
|
return Number.isFinite(n) ? n : 0;
|
|
};
|
|
|
|
/** First positive numeric value among the given keys, else 0. */
|
|
const firstPositive = (item: any, keys: string[]): number => {
|
|
for (const k of keys) {
|
|
const n = num(item?.[k]);
|
|
if (n > 0) return n;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
// The bid-variation amount can arrive under any of these keys depending on the
|
|
// endpoint; whichever is present and positive is the authoritative step.
|
|
const STEP_KEYS = [
|
|
'bidVariationValue',
|
|
'bidVariation',
|
|
'variationValue',
|
|
'bidVariationFigure',
|
|
'incrementValue',
|
|
'decrementValue',
|
|
];
|
|
|
|
/**
|
|
* The +/- step for an item:
|
|
* - the configured bid-variation amount when the backend provides one,
|
|
* - otherwise a flat ₹1, so a misconfigured auction (variation 0/absent) can
|
|
* never leave the stepper frozen and the user can still nudge the bid.
|
|
*/
|
|
const resolveStep = (item: any): number => {
|
|
const configured = firstPositive(item, STEP_KEYS);
|
|
return configured > 0 ? configured : 1;
|
|
};
|
|
|
|
const clamp = (v: number, lo: number, hi: number): number => Math.min(Math.max(v, lo), hi);
|
|
|
|
const forward: BiddingPatternConfig = {
|
|
pattern: 'Forward',
|
|
itemsUrl: FORWARD_AUCTION_ITEM,
|
|
startValue: item => num(item?.auctionStartValueFigure),
|
|
step: item => resolveStep(item),
|
|
stepLabel: 'Increment',
|
|
stepPrefix: '+',
|
|
// Forward: outbid upward. Never below the current bid; no upper limit.
|
|
floor: item => num(item?.bidAmount),
|
|
ceil: () => Number.POSITIVE_INFINITY,
|
|
stepUp: (bidValue, item) => clamp(num(bidValue) + forward.step(item), forward.floor(item), forward.ceil(item)),
|
|
stepDown: (bidValue, item) => clamp(num(bidValue) - forward.step(item), forward.floor(item), forward.ceil(item)),
|
|
canIncrease: (bidValue, item) => forward.stepUp(bidValue, item) > num(bidValue),
|
|
canDecrease: (bidValue, item) => forward.stepDown(bidValue, item) < num(bidValue),
|
|
acceptManualInput: () => true,
|
|
validateManualBid: (value, item) =>
|
|
num(value) < num(item?.bidAmount) ? 'Enter a value greater than the current bid value' : null,
|
|
inlineManualInput: false,
|
|
isBodyFull: (flags, item) => !flags.rebid && item.contractorId == undefined,
|
|
showSubmit: (flags, item) => !flags.rebid && item.contractorId == undefined,
|
|
};
|
|
|
|
const reverse: BiddingPatternConfig = {
|
|
pattern: 'Reverse',
|
|
itemsUrl: REVERSE_AUCTION_ITEM,
|
|
startValue: item => num(item?.auctionStartValue),
|
|
step: item => resolveStep(item),
|
|
stepLabel: 'Decrement',
|
|
stepPrefix: '-',
|
|
// Reverse: undercut downward. Never above the current bid; never below EMD.
|
|
floor: item => num(item?.emdAmount),
|
|
ceil: item => num(item?.bidAmount) || num(item?.auctionStartValue),
|
|
stepUp: (bidValue, item) => clamp(num(bidValue) + reverse.step(item), reverse.floor(item), reverse.ceil(item)),
|
|
stepDown: (bidValue, item) => clamp(num(bidValue) - reverse.step(item), reverse.floor(item), reverse.ceil(item)),
|
|
canIncrease: (bidValue, item) => reverse.stepUp(bidValue, item) > num(bidValue),
|
|
canDecrease: (bidValue, item) => reverse.stepDown(bidValue, item) < num(bidValue),
|
|
acceptManualInput: (value, item) => num(value) >= 0 && num(value) <= num(item?.auctionStartValue),
|
|
validateManualBid: () => null,
|
|
inlineManualInput: true,
|
|
isBodyFull: () => true,
|
|
showSubmit: () => true,
|
|
};
|
|
|
|
export const getBiddingConfig = (pattern: string | undefined): BiddingPatternConfig =>
|
|
pattern === 'Reverse' ? reverse : forward;
|