perf: project-wide optimization pass + UI/theme fixes
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7853bab639
commit
95d8e0a925
@@ -42,12 +42,15 @@ const AuctionBiddingScreen = () => {
|
||||
const { colors, spacing, shadows } = useTheme();
|
||||
const SelectedAuction = useSelector((state: any) => state.auction.auction);
|
||||
const loginUser = useSelector((state: any) => state.token.token);
|
||||
// The login session stores the contractor id under `id` (see ProfileTab /
|
||||
// LiveTender); the older `contractorId` key is absent, which is why bid
|
||||
// payloads and "me"/LEADING detection were getting undefined.
|
||||
const contractorId = loginUser?.contractorId ?? loginUser?.id;
|
||||
const Navigation: any = useNavigation();
|
||||
const config = useMemo(
|
||||
() => getBiddingConfig(SelectedAuction?.auctionPattern),
|
||||
[SelectedAuction?.auctionPattern],
|
||||
);
|
||||
|
||||
const [items, setItems] = useState<any>([]);
|
||||
const [bidValues, setBidValues] = useState<Record<number, number>>({});
|
||||
const [allBoolean, setAllBoolean] = useState({
|
||||
@@ -104,11 +107,11 @@ const AuctionBiddingScreen = () => {
|
||||
...item,
|
||||
bidAmount: bidData?.bidAmount ?? config.startValue(item),
|
||||
bidAvilable: bidData?.bidAmount ?? 0,
|
||||
me: bidData?.contractorId === loginUser.contractorId,
|
||||
me: bidData?.contractorId === contractorId,
|
||||
};
|
||||
});
|
||||
const initialBids: Record<number, number> = {};
|
||||
updatedFilteredItems.forEach(item => { initialBids[item.id] = item.bidAmount; });
|
||||
updatedFilteredItems.forEach(item => { initialBids[item.id] = Number(item.bidAmount); });
|
||||
setBidValues(initialBids);
|
||||
setItems(updatedFilteredItems);
|
||||
} else {
|
||||
@@ -125,12 +128,12 @@ const AuctionBiddingScreen = () => {
|
||||
bidId: bid.bidId,
|
||||
contractorId: bid.contractorId,
|
||||
contractorName: bid.contractorName,
|
||||
me: bid.contractorId === loginUser.contractorId,
|
||||
me: bid.contractorId === contractorId,
|
||||
bidTime: bid.bidTime,
|
||||
};
|
||||
});
|
||||
const initialBids: Record<number, number> = {};
|
||||
mergedData.forEach(item => { initialBids[item.id] = item.bidAmount; });
|
||||
mergedData.forEach(item => { initialBids[item.id] = Number(item.bidAmount); });
|
||||
setBidValues(initialBids);
|
||||
setItems(mergedData);
|
||||
}
|
||||
@@ -152,7 +155,7 @@ const AuctionBiddingScreen = () => {
|
||||
: item,
|
||||
),
|
||||
);
|
||||
setBidValues(prev => ({ ...prev, [result.auctionItemId]: result.bidAmount }));
|
||||
setBidValues(prev => ({ ...prev, [result.auctionItemId]: Number(result.bidAmount) }));
|
||||
});
|
||||
return () => unsubscribe(destination);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -170,8 +173,19 @@ const AuctionBiddingScreen = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [allBoolean.reload]);
|
||||
|
||||
const handleUpdate = (id: number, val: number) => {
|
||||
setBidValues(prev => ({ ...prev, [id]: prev[id] + val }));
|
||||
// Move a bid one step in the given direction. The next value is computed from
|
||||
// the LIVE `prev` state (no stale closure on rapid taps) and clamped to the
|
||||
// pattern's [floor, ceil] by the config, so the button always moves the value
|
||||
// by a real amount and can never overshoot a bound.
|
||||
const stepBid = (item: any, direction: 'up' | 'down') => {
|
||||
setBidValues(prev => {
|
||||
let current = Number(prev[item.id]);
|
||||
if (!Number.isFinite(current)) { current = config.startValue(item); }
|
||||
const next = direction === 'up'
|
||||
? config.stepUp(current, item)
|
||||
: config.stepDown(current, item);
|
||||
return { ...prev, [item.id]: next };
|
||||
});
|
||||
};
|
||||
|
||||
const submitBid = async (item: any) => {
|
||||
@@ -179,7 +193,7 @@ const AuctionBiddingScreen = () => {
|
||||
itemId: item.id,
|
||||
bidAmount: bidValues[item.id],
|
||||
auctionId: SelectedAuction.auctionId,
|
||||
contractorId: loginUser.contractorId,
|
||||
contractorId,
|
||||
fullName: loginUser.fullName,
|
||||
roomId: SelectedAuction.auctionNumber,
|
||||
};
|
||||
@@ -212,7 +226,7 @@ const AuctionBiddingScreen = () => {
|
||||
}, []),
|
||||
);
|
||||
|
||||
const styles = makeStyles(colors, spacing, shadows);
|
||||
const styles = useMemo(() => makeStyles(colors, spacing, shadows), [colors, spacing, shadows]);
|
||||
|
||||
const biddingEndDate =
|
||||
SelectedAuction?.auctionStatus === 'Amended'
|
||||
@@ -280,8 +294,8 @@ const AuctionBiddingScreen = () => {
|
||||
<AuctionBiddingCard
|
||||
item={{ ...item, index }}
|
||||
bidValue={bidValues[item.id] || 0}
|
||||
onIncrease={() => handleUpdate(item.id, config.step(item))}
|
||||
onDecrease={() => handleUpdate(item.id, -config.step(item))}
|
||||
onIncrease={() => stepBid(item, 'up')}
|
||||
onDecrease={() => stepBid(item, 'down')}
|
||||
onPlaceBid={() => submitBid(item)}
|
||||
SelectedAuction={SelectedAuction}
|
||||
allBoolean={allBoolean}
|
||||
|
||||
@@ -43,7 +43,6 @@ const AuctionBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
|
||||
const canDecrease = config.canDecrease(bidValue, item);
|
||||
const bodyFull = config.isBodyFull(allBoolean, item);
|
||||
const submitVisible = config.showSubmit(allBoolean, item);
|
||||
|
||||
// 🔒 Persist expand state safely
|
||||
const expandedRef = useRef(false);
|
||||
const animation = useSharedValue(0);
|
||||
@@ -98,7 +97,8 @@ const AuctionBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
|
||||
itemId: item.id,
|
||||
bidAmount: Number(value),
|
||||
auctionId: SelectedAuction.auctionId,
|
||||
contractorId: Token.contractorId,
|
||||
// Session stores the contractor id as `id` (contractorId is absent).
|
||||
contractorId: Token?.contractorId ?? Token?.id,
|
||||
fullName: Token.fullName,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -108,7 +108,6 @@ const Emd = () => {
|
||||
|
||||
/* ── Render Item ── */
|
||||
const renderItem = ({ item }: any) => {
|
||||
console.log(item, "item");
|
||||
const selected = isSelected(item.id);
|
||||
const selectedData = selectedItems.find(i => i.auctionItemId === item.id);
|
||||
|
||||
@@ -352,7 +351,7 @@ const Emd = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => { AlreadyEmdCheck(); }, []);
|
||||
|
||||
const styles = makeStyles(colors, spacing, shadows, typography);
|
||||
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
|
||||
|
||||
/* ── Policy + Submit footer ── */
|
||||
const BottomBar = ({ onSubmit }: { onSubmit: () => void }) => (
|
||||
|
||||
@@ -53,17 +53,24 @@ describe('forward pattern', () => {
|
||||
|
||||
describe('reverse pattern', () => {
|
||||
const cfg = getBiddingConfig('Reverse');
|
||||
const item = { auctionStartValue: 1000, emdAmount: 50, bidAmount: 800 };
|
||||
const item = { auctionStartValue: 1000, emdAmount: 50, bidAmount: 800, bidVariationValue: 50 };
|
||||
|
||||
it('reads the reverse start-value and step fields', () => {
|
||||
expect(cfg.itemsUrl).toBe(REVERSE_AUCTION_ITEM);
|
||||
expect(cfg.startValue(item)).toBe(1000);
|
||||
expect(cfg.step(item)).toBe(50);
|
||||
expect(cfg.step(item)).toBe(50); // the configured bid-variation
|
||||
expect(cfg.stepLabel).toBe('Decrement');
|
||||
expect(cfg.stepPrefix).toBe('-');
|
||||
expect(cfg.inlineManualInput).toBe(true);
|
||||
});
|
||||
|
||||
it('decrements by bidVariationValue, not the EMD deposit', () => {
|
||||
// Real payloads send amounts as strings; the decrement is the bid variation.
|
||||
expect(cfg.step({ bidVariationValue: '11', emdAmount: '0' })).toBe(11);
|
||||
// With no bid-variation, it does NOT use the EMD deposit — it steps by ₹1.
|
||||
expect(cfg.step({ emdAmount: '50' })).toBe(1);
|
||||
});
|
||||
|
||||
it('caps increases at the current bid and decreases at the EMD floor', () => {
|
||||
expect(cfg.canIncrease(700, item)).toBe(true); // 700 < 800
|
||||
expect(cfg.canIncrease(800, item)).toBe(false);
|
||||
@@ -84,3 +91,44 @@ describe('reverse pattern', () => {
|
||||
expect(cfg.validateManualBid(123, item)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('stepper robustness (both patterns)', () => {
|
||||
const rev = getBiddingConfig('Reverse');
|
||||
const fwd = getBiddingConfig('Forward');
|
||||
|
||||
it('never resolves a zero/NaN step, even when every amount field is missing', () => {
|
||||
// This is the exact "DECREMENT -₹0 / button does nothing" bug.
|
||||
expect(rev.step({ auctionStartValue: '550', bidAmount: '539', emdAmount: '0' })).toBeGreaterThan(0);
|
||||
expect(rev.step({})).toBeGreaterThanOrEqual(1);
|
||||
expect(fwd.step({})).toBeGreaterThanOrEqual(1);
|
||||
// With no configured bid-variation the step is a flat ₹1.
|
||||
expect(rev.step({ auctionStartValue: 1000 })).toBe(1);
|
||||
expect(fwd.step({ auctionStartValueFigure: 200 })).toBe(1);
|
||||
});
|
||||
|
||||
it('reverse stepDown moves and clamps to the EMD floor without overshooting', () => {
|
||||
const it = { auctionStartValue: 1000, emdAmount: 50, bidAmount: 800, bidVariationValue: 50 };
|
||||
expect(rev.stepDown(800, it)).toBe(750);
|
||||
expect(rev.stepDown(60, it)).toBe(50); // 60-50=10 would undershoot -> clamped to floor
|
||||
expect(rev.stepDown(50, it)).toBe(50); // already at floor
|
||||
});
|
||||
|
||||
it('reverse stepUp clamps to the current bid (ceiling)', () => {
|
||||
const it = { auctionStartValue: 1000, emdAmount: 50, bidAmount: 800, bidVariationValue: 50 };
|
||||
expect(rev.stepUp(760, it)).toBe(800); // 760+50=810 would exceed -> clamped to ceil
|
||||
expect(rev.stepUp(800, it)).toBe(800); // already at ceil
|
||||
});
|
||||
|
||||
it('forward stepUp is unbounded and stepDown floors at the current bid', () => {
|
||||
const it = { auctionStartValueFigure: 100, bidVariationValue: 10, bidAmount: 150 };
|
||||
expect(fwd.stepUp(150, it)).toBe(160);
|
||||
expect(fwd.stepDown(160, it)).toBe(150);
|
||||
expect(fwd.stepDown(150, it)).toBe(150); // can't undercut the current bid
|
||||
});
|
||||
|
||||
it('a reverse item with no variation still decrements (regression for -₹0)', () => {
|
||||
const it = { auctionStartValue: '550', bidAmount: '539', emdAmount: '0' };
|
||||
expect(rev.canDecrease(539, it)).toBe(true);
|
||||
expect(rev.stepDown(539, it)).toBeLessThan(539);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
* 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';
|
||||
|
||||
@@ -21,13 +28,19 @@ export type BiddingPatternConfig = {
|
||||
itemsUrl: string;
|
||||
/** Opening amount — the backend uses a different field per pattern. */
|
||||
startValue: (item: any) => number;
|
||||
/** Stepper step size. */
|
||||
/** Stepper step size — always a positive number (see resolveStep). */
|
||||
step: (item: any) => number;
|
||||
stepLabel: 'Increment' | 'Decrement';
|
||||
stepPrefix: '+' | '-';
|
||||
/** Forward bids may rise without bound; reverse bids may not exceed the current bid. */
|
||||
/** 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;
|
||||
/** Forward bids may not drop below the current bid; reverse not below the EMD amount. */
|
||||
canDecrease: (bidValue: number, item: any) => boolean;
|
||||
/** Gate applied while typing a manual (one-time/rebid) amount. */
|
||||
acceptManualInput: (value: number, item: any) => boolean;
|
||||
@@ -41,18 +54,67 @@ export type BiddingPatternConfig = {
|
||||
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 => item?.auctionStartValueFigure,
|
||||
step: item => item?.bidVariationValue,
|
||||
startValue: item => num(item?.auctionStartValueFigure),
|
||||
step: item => resolveStep(item),
|
||||
stepLabel: 'Increment',
|
||||
stepPrefix: '+',
|
||||
canIncrease: () => true,
|
||||
canDecrease: (bidValue, item) => bidValue > item.bidAmount,
|
||||
// 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) =>
|
||||
value < Number(item.bidAmount) ? 'Enter a value greater than the current bid value' : null,
|
||||
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,
|
||||
@@ -61,13 +123,18 @@ const forward: BiddingPatternConfig = {
|
||||
const reverse: BiddingPatternConfig = {
|
||||
pattern: 'Reverse',
|
||||
itemsUrl: REVERSE_AUCTION_ITEM,
|
||||
startValue: item => item?.auctionStartValue,
|
||||
step: item => item?.emdAmount,
|
||||
startValue: item => num(item?.auctionStartValue),
|
||||
step: item => resolveStep(item),
|
||||
stepLabel: 'Decrement',
|
||||
stepPrefix: '-',
|
||||
canIncrease: (bidValue, item) => bidValue < item.bidAmount,
|
||||
canDecrease: (bidValue, item) => bidValue > item.emdAmount,
|
||||
acceptManualInput: (value, item) => value >= 0 && value <= Number(item?.auctionStartValue),
|
||||
// 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,
|
||||
|
||||
Reference in New Issue
Block a user