feat: architecture refactor — core layer, spreadsheet engine, envelope bidding, tests
CI / quality (push) Waiting to run
CI / quality (push) Waiting to run
- Add src/core/ (config, errors, events, http, observability, storage, query) - Add src/Spreadsheet/ engine with native POI backend and Excel bridge - Add Envelope C spreadsheet flow and tender bidding envelope modules - Consolidate components into src/components/, remove legacy Component/ and Hooks/ - Add registration state/validation, auth session guard, profile edit modules - Add unit tests across axios, redux, bidding, registration, encryption, core - Add CI workflow, docs (architecture, audit, DSC API spec), keystore example - Remove tracked keystore binary; ignore scripts.zip Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
9acec328a7
commit
1d7646d938
+75
-56
@@ -1,40 +1,52 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
Alert,
|
||||
BackHandler,
|
||||
} from 'react-native';
|
||||
import LottieView from 'lottie-react-native';
|
||||
import Animated, { FadeInDown } from 'react-native-reanimated';
|
||||
import { InsideHeader } from '../../Component/Index';
|
||||
import { InsideHeader } from '../../components';
|
||||
import { useWebSocketService } from '../../socket/Socket';
|
||||
import Variables from '../../Constant/Variable';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { apiCall } from '../../Axios/Axios';
|
||||
import {
|
||||
AUCTION_ONE_TIME_BID,
|
||||
FORWARD_AUCTION_ITEM,
|
||||
GET_AUCTION_DETAILS,
|
||||
REVERSE_AUCTION_ITEM,
|
||||
SELECTED_AUCTION_ITEM,
|
||||
} from '../../Services/ApiCall/ApiUrls';
|
||||
import BiddingCard from './Component/BiddingCard';
|
||||
import AuctionBiddingCard from './Component/AuctionBiddingCard';
|
||||
import ConfirmBidModal from './Component/ConfirmBidModal';
|
||||
import GoBackModal from './Component/GoBackModal';
|
||||
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||
import NavigationString from '../../Constant/NavigationString';
|
||||
import CountdownTimer from '../../Component/Counter/CountdownTimer';
|
||||
import CountdownTimer from '../../components/CountdownTimer';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
import { Gavel, Building2, Clock4 } from 'lucide-react-native';
|
||||
import { showErrorAlert, showSuccessAlert } from '../../Constant/ShowDailog';
|
||||
import { logger } from '../../core/observability/logger';
|
||||
import { getBiddingConfig } from './biddingPattern';
|
||||
|
||||
const StartBidding = () => {
|
||||
/**
|
||||
* THE bidding panel — serves both auction patterns. The pattern comes from
|
||||
* the selected auction; every forward/reverse difference lives in
|
||||
* biddingPattern.ts, never in this file.
|
||||
*
|
||||
* Registered under both the StartBidding and StartReverseBidding routes so
|
||||
* existing navigation keeps working unchanged.
|
||||
*/
|
||||
const AuctionBiddingScreen = () => {
|
||||
const { colors, spacing, shadows } = useTheme();
|
||||
const SelectedAuction = useSelector((state: any) => state.auction.auction);
|
||||
const loginUser = useSelector((state: any) => state.token.token);
|
||||
const Navigation: any = useNavigation();
|
||||
const config = useMemo(
|
||||
() => getBiddingConfig(SelectedAuction?.auctionPattern),
|
||||
[SelectedAuction?.auctionPattern],
|
||||
);
|
||||
|
||||
const [items, setItems] = useState<any>([]);
|
||||
const [bidValues, setBidValues] = useState<Record<number, number>>({});
|
||||
@@ -48,11 +60,10 @@ const StartBidding = () => {
|
||||
reload: false,
|
||||
});
|
||||
|
||||
const { subscribe, createRoom, placeBid } = useWebSocketService(
|
||||
const { subscribe, unsubscribe, disconnect, createRoom, placeBid } = useWebSocketService(
|
||||
Variables.webSocketUrl,
|
||||
() => console.log('Connected!'),
|
||||
(error: any) => console.log('WebSocket Error:', error),
|
||||
loginUser.token,
|
||||
() => logger.info('stomp', 'connected'),
|
||||
(error: any) => logger.warn('stomp', `connection error: ${error}`),
|
||||
);
|
||||
|
||||
const getAuction = async () => {
|
||||
@@ -74,10 +85,8 @@ const StartBidding = () => {
|
||||
const payload = { auctionNumber: SelectedAuction.auctionNumber };
|
||||
let auctionItem: any[] = [];
|
||||
|
||||
if (SelectedAuction.auctionPattern === 'Forward') {
|
||||
const res = await apiCall<any>('get', FORWARD_AUCTION_ITEM, {}, payload);
|
||||
if (res?.statusCode === 200) auctionItem = res.data || [];
|
||||
}
|
||||
const res = await apiCall<any>('get', config.itemsUrl, {}, payload);
|
||||
if (res?.statusCode === 200) { auctionItem = res.data || []; }
|
||||
|
||||
const selectedRes = await apiCall<any>('get', SELECTED_AUCTION_ITEM, {}, payload);
|
||||
const uniqueArray: number[] = Array.from(new Set(selectedRes?.data || []));
|
||||
@@ -85,18 +94,15 @@ const StartBidding = () => {
|
||||
|
||||
if (SelectedAuction.auctionCategories === 'Live') {
|
||||
const result = await createRoom(SelectedAuction.auctionNumber);
|
||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, (data: any) => {
|
||||
console.log('🔥 BID UPDATE:', data);
|
||||
});
|
||||
const bidAmountMap2 = new Map<number, { bidAmount: number; contractorId: number }>();
|
||||
const liveBids = new Map<number, { bidAmount: number; contractorId: number }>();
|
||||
(result || []).forEach((r: any) => {
|
||||
bidAmountMap2.set(r.auctionItemId, { bidAmount: r.bidAmount, contractorId: r.contractorId });
|
||||
liveBids.set(r.auctionItemId, { bidAmount: r.bidAmount, contractorId: r.contractorId });
|
||||
});
|
||||
const updatedFilteredItems = filteredItems.map(item => {
|
||||
const bidData = bidAmountMap2.get(item.id);
|
||||
const bidData = liveBids.get(item.id);
|
||||
return {
|
||||
...item,
|
||||
bidAmount: bidData?.bidAmount ?? item.auctionStartValueFigure,
|
||||
bidAmount: bidData?.bidAmount ?? config.startValue(item),
|
||||
bidAvilable: bidData?.bidAmount ?? 0,
|
||||
me: bidData?.contractorId === loginUser.contractorId,
|
||||
};
|
||||
@@ -106,14 +112,13 @@ const StartBidding = () => {
|
||||
setBidValues(initialBids);
|
||||
setItems(updatedFilteredItems);
|
||||
} else {
|
||||
if (SelectedAuction.rebidAllowed) setAllBoolean(prev => ({ ...prev, rebid: true }));
|
||||
let result: any[] = [];
|
||||
if (SelectedAuction.auctionCategories !== 'Live') result = await getOneTimeBidValue();
|
||||
if (SelectedAuction.rebidAllowed) { setAllBoolean(prev => ({ ...prev, rebid: true })); }
|
||||
const result: any[] = await getOneTimeBidValue();
|
||||
const bidMap = new Map<number, any>();
|
||||
result.forEach((bid: any) => bidMap.set(bid.auctionItemId, bid));
|
||||
const mergedData = auctionItem.map((item: any) => {
|
||||
const bid = bidMap.get(item.id);
|
||||
if (!bid) return { ...item, bidAmount: item.auctionStartValueFigure, contractorId: item.contractorId };
|
||||
if (!bid) { return { ...item, bidAmount: config.startValue(item), contractorId: item.contractorId }; }
|
||||
return {
|
||||
...item,
|
||||
bidAmount: bid.bidAmount,
|
||||
@@ -130,34 +135,41 @@ const StartBidding = () => {
|
||||
setItems(mergedData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching auction items:', error);
|
||||
logger.error('bidding', 'fetching auction items failed', { error: String(error) });
|
||||
}
|
||||
};
|
||||
|
||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, async (message: any) => {
|
||||
const result = message[message.length - 1];
|
||||
setItems((prev: any[]) =>
|
||||
prev.map(item =>
|
||||
item.id === result.auctionItemId
|
||||
? { ...item, bidAmount: result.bidAmount, me: result.contractorId === loginUser.contractorId }
|
||||
: item,
|
||||
),
|
||||
);
|
||||
setBidValues(prev => ({ ...prev, [result.auctionItemId]: result.bidAmount }));
|
||||
});
|
||||
// Live bid updates: subscribe once per room from an effect (never the render
|
||||
// body) and tear the subscription down when the room changes or we unmount.
|
||||
useEffect(() => {
|
||||
const destination = `/topic/room/${SelectedAuction.auctionNumber}`;
|
||||
subscribe(destination, (message: any) => {
|
||||
const result = message[message.length - 1];
|
||||
setItems((prev: any[]) =>
|
||||
prev.map(item =>
|
||||
item.id === result.auctionItemId
|
||||
? { ...item, bidAmount: result.bidAmount, me: result.contractorId === loginUser.contractorId }
|
||||
: item,
|
||||
),
|
||||
);
|
||||
setBidValues(prev => ({ ...prev, [result.auctionItemId]: result.bidAmount }));
|
||||
});
|
||||
return () => unsubscribe(destination);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [SelectedAuction.auctionNumber]);
|
||||
|
||||
// Fully close the STOMP connection on unmount — a leaked client keeps
|
||||
// reconnecting every 5s and drains battery/data.
|
||||
const disconnectRef = useRef(disconnect);
|
||||
disconnectRef.current = disconnect;
|
||||
useEffect(() => () => disconnectRef.current(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (SelectedAuction.auctionCategories === 'One-time') getAuction();
|
||||
if (SelectedAuction.auctionCategories === 'One-time') { getAuction(); }
|
||||
getAllAcutionItems();
|
||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, () => {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [allBoolean.reload]);
|
||||
|
||||
useEffect(() => {
|
||||
const initial: Record<number, number> = {};
|
||||
items.forEach((item: any) => { initial[item.id] = item.bidAmount + item.bidVariationValue; });
|
||||
setBidValues(initial);
|
||||
}, []);
|
||||
|
||||
const handleUpdate = (id: number, val: number) => {
|
||||
setBidValues(prev => ({ ...prev, [id]: prev[id] + val }));
|
||||
};
|
||||
@@ -177,10 +189,15 @@ const StartBidding = () => {
|
||||
const placeBidSubmit = async (data: any) => {
|
||||
setAllBoolean(prev => ({ ...prev, showDialog: false }));
|
||||
try {
|
||||
const result: any = await placeBid(data);
|
||||
if (result?.status && result.data) getAllAcutionItems();
|
||||
const result = await placeBid(data);
|
||||
if (result?.status && result.data) {
|
||||
if (result.message) { showSuccessAlert(result.message); }
|
||||
getAllAcutionItems();
|
||||
} else if (result?.message) {
|
||||
showErrorAlert(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error placing bid:', error);
|
||||
logger.error('bidding', 'placing bid failed', { error: String(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -192,7 +209,7 @@ const StartBidding = () => {
|
||||
};
|
||||
const sub = BackHandler.addEventListener('hardwareBackPress', onBackPress);
|
||||
return () => sub.remove();
|
||||
}, [Navigation]),
|
||||
}, []),
|
||||
);
|
||||
|
||||
const styles = makeStyles(colors, spacing, shadows);
|
||||
@@ -260,15 +277,16 @@ const StartBidding = () => {
|
||||
data={items}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
renderItem={({ item, index }) => (
|
||||
<BiddingCard
|
||||
<AuctionBiddingCard
|
||||
item={{ ...item, index }}
|
||||
bidValue={bidValues[item.id] || 0}
|
||||
onIncrease={() => handleUpdate(item.id, item.bidVariationValue)}
|
||||
onDecrease={() => handleUpdate(item.id, -item.bidVariationValue)}
|
||||
onIncrease={() => handleUpdate(item.id, config.step(item))}
|
||||
onDecrease={() => handleUpdate(item.id, -config.step(item))}
|
||||
onPlaceBid={() => submitBid(item)}
|
||||
SelectedAuction={SelectedAuction}
|
||||
allBoolean={allBoolean}
|
||||
setAllBoolean={setAllBoolean}
|
||||
config={config}
|
||||
/>
|
||||
)}
|
||||
contentContainerStyle={styles.listContent}
|
||||
@@ -282,7 +300,7 @@ const StartBidding = () => {
|
||||
source={require('../../lottie/Success.json')}
|
||||
autoPlay
|
||||
loop={false}
|
||||
style={{ width: 200, height: 200 }}
|
||||
style={styles.successLottie}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
@@ -311,7 +329,7 @@ const StartBidding = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default StartBidding;
|
||||
export default AuctionBiddingScreen;
|
||||
|
||||
/* ─── Styles factory ─── */
|
||||
const makeStyles = (colors: any, spacing: any, shadows: any) =>
|
||||
@@ -399,4 +417,5 @@ const makeStyles = (colors: any, spacing: any, shadows: any) =>
|
||||
alignItems: 'center',
|
||||
zIndex: 99,
|
||||
},
|
||||
successLottie: { width: 200, height: 200 },
|
||||
});
|
||||
+123
-73
@@ -1,25 +1,48 @@
|
||||
import React, { memo, useCallback, useRef, useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput, Alert } from 'react-native';
|
||||
import { View, TouchableOpacity, StyleSheet, TextInput } from 'react-native';
|
||||
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
||||
import colors_const from '../../../Constant/Color';
|
||||
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
|
||||
import MainStyle from '../../../Styles/MainStyle';
|
||||
import { useTheme } from '../../../Hooks/useTheme';
|
||||
import AppText from '../../../components/AppText';
|
||||
import { Loader } from '../../../Component/Index';
|
||||
import { Loader } from '../../../components';
|
||||
import { useSelector } from 'react-redux';
|
||||
import axios from 'axios';
|
||||
import Variables from '../../../Constant/Variable';
|
||||
import { env } from '../../../core/config/env';
|
||||
import { request } from '../../../core/http/client';
|
||||
import { isAppError } from '../../../core/http/AppError';
|
||||
import { logger } from '../../../core/observability/logger';
|
||||
import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
|
||||
import { BiddingPatternConfig } from '../biddingPattern';
|
||||
|
||||
const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => {
|
||||
type Props = {
|
||||
item: any;
|
||||
bidValue: number;
|
||||
onIncrease: () => void;
|
||||
onDecrease: () => void;
|
||||
onPlaceBid: () => void;
|
||||
SelectedAuction: any;
|
||||
allBoolean: any;
|
||||
setAllBoolean: React.Dispatch<React.SetStateAction<any>>;
|
||||
config: BiddingPatternConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* The single bidding card for BOTH auction patterns. All forward/reverse
|
||||
* behavior differences come from `config` (see biddingPattern.ts) — never
|
||||
* branch on the pattern inside this component.
|
||||
*/
|
||||
const AuctionBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean, config }: Props) => {
|
||||
const { colors } = useTheme();
|
||||
const styles = React.useMemo(() => makeStyles(colors), [colors]);
|
||||
const Token = useSelector((state: any) => state.token.token)
|
||||
const Token = useSelector((state: any) => state.token.token);
|
||||
const [showDetail, setShowDetail] = useState(false);
|
||||
const [value, setValue] = useState(`${item.bidAmount}`);
|
||||
const [allState, setAllState] = useState({ isLoading: false })
|
||||
const isMin = bidValue <= item.bidAmount;
|
||||
const [allState, setAllState] = useState({ isLoading: false });
|
||||
|
||||
const canIncrease = config.canIncrease(bidValue, item);
|
||||
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);
|
||||
@@ -31,7 +54,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
||||
damping: 15,
|
||||
stiffness: 140,
|
||||
});
|
||||
}, []);
|
||||
}, [animation]);
|
||||
|
||||
const bodyStyle = useAnimatedStyle(() => ({
|
||||
height: interpolate(animation.value, [0, 1], [0, 190]),
|
||||
@@ -51,22 +74,26 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
||||
},
|
||||
],
|
||||
}));
|
||||
const MAX_VALUE = Number(item.bidAmount);
|
||||
|
||||
const handleChange = (text: string) => {
|
||||
let cleaned = text.replace(/[^0-9.]/g, '');
|
||||
if ((cleaned.match(/\./g) || []).length > 1) return;
|
||||
const cleaned = text.replace(/[^0-9.]/g, '');
|
||||
if ((cleaned.match(/\./g) || []).length > 1) { return; }
|
||||
if (cleaned === '') {
|
||||
setValue('');
|
||||
return;
|
||||
}
|
||||
if (!config.acceptManualInput(Number(cleaned), item)) { return; }
|
||||
setValue(cleaned);
|
||||
};
|
||||
|
||||
|
||||
const oneTimePlaceBid = async () => {
|
||||
const validationError = config.validateManualBid(Number(value), item);
|
||||
if (validationError) {
|
||||
showErrorAlert(validationError);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (Number(value) < MAX_VALUE) {
|
||||
showErrorAlert("Enter the greather vlaue to current bid value")
|
||||
return false
|
||||
}
|
||||
setAllState((prev: any) => ({ ...prev, isLoading: true }))
|
||||
setAllState((prev: any) => ({ ...prev, isLoading: true }));
|
||||
const payload = {
|
||||
itemId: item.id,
|
||||
bidAmount: Number(value),
|
||||
@@ -75,28 +102,41 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
||||
fullName: Token.fullName,
|
||||
};
|
||||
|
||||
const res = await request<any>({
|
||||
method: 'post',
|
||||
url: `${env.socketBaseUrl}bidding/placeBidForAuction`,
|
||||
data: [payload],
|
||||
});
|
||||
|
||||
const res = await axios.post(
|
||||
`${Variables.socketUrl}bidding/placeBidForAuction`,
|
||||
[payload],
|
||||
{ headers: { Authentication: `Bearer ${Token.token}` } }
|
||||
);
|
||||
|
||||
if (res.data.statusCode === 200) {
|
||||
showSuccessAlert(res.data.message);
|
||||
if (res.statusCode === 200) {
|
||||
showSuccessAlert(res.message);
|
||||
}
|
||||
setAllBoolean((prev: any) => ({ ...prev, reload: !prev.reload }))
|
||||
setAllBoolean((prev: any) => ({ ...prev, reload: !prev.reload }));
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
showErrorAlert(error.response?.data.message)
|
||||
if (isAppError(error) && error.statusCode !== undefined) {
|
||||
showErrorAlert(error.message);
|
||||
} else {
|
||||
console.log('UNKNOWN ERROR:', error);
|
||||
logger.error('bidding', 'manual bid failed', { error: String(error) });
|
||||
}
|
||||
|
||||
} finally {
|
||||
setAllState((prev: any) => ({ ...prev, isLoading: false }))
|
||||
setAllState((prev: any) => ({ ...prev, isLoading: false }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const manualInput = (allBoolean.oneTimeBid || allBoolean.rebid) && (
|
||||
<View style={[styles.inputBox2, config.inlineManualInput ? styles.inputBox2Inline : styles.inputBox2Block]}>
|
||||
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency2 as any}>₹</AppText>
|
||||
|
||||
<TextInput
|
||||
value={value}
|
||||
onChangeText={handleChange}
|
||||
keyboardType="decimal-pad"
|
||||
placeholder="0.00"
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View>
|
||||
@@ -109,19 +149,18 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
||||
{/* HEADER */}
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={() => { setShowDetail(true) }}
|
||||
onPress={() => { setShowDetail(true); }}
|
||||
style={styles.cardHeader}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={styles.flex1}>
|
||||
<AppText variant="h4" color={colors.text} bold style={styles.productName as any}>{item.productName}</AppText>
|
||||
{/* auctionStartValueFigure */}
|
||||
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
|
||||
Initial Amt : {item?.auctionStartValueFigure}
|
||||
Initial Amt : ₹{config.startValue(item)}
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
|
||||
<View style={[MainStyle.flexBetween, { gap: 10 }]}>
|
||||
<View style={[MainStyle.flexBetween, styles.gap10]}>
|
||||
<View style={[styles.badge, item.me ? styles.meBadge : styles.liveBadge]}>
|
||||
<AppText variant="caption" bold style={[styles.badgeText as any, item.me ? styles.meText : styles.liveText]}>
|
||||
{item.me ? 'LEADING' : 'LIVE'}
|
||||
@@ -143,77 +182,83 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
||||
<View style={styles.priceRow}>
|
||||
<View>
|
||||
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Current Bid</AppText>
|
||||
<AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item?.bidAmount || item.auctionStartValueFigure}</AppText>
|
||||
<AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item?.bidAmount || config.startValue(item)}</AppText>
|
||||
</View>
|
||||
|
||||
<View style={{ alignItems: 'flex-end' }}>
|
||||
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Increment</AppText>
|
||||
<View style={styles.alignEnd}>
|
||||
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>{config.stepLabel}</AppText>
|
||||
<AppText variant="body" color={colors.primary} bold style={styles.incrementText as any}>
|
||||
+₹{item.bidVariationValue}
|
||||
{config.stepPrefix}₹{config.step(item)}
|
||||
</AppText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* COLLAPSIBLE BODY */}
|
||||
<Animated.View style={[styles.collapsibleContainer, (!allBoolean.rebid && item.contractorId == undefined) ? bodyStyle : bodyStyle2]}>
|
||||
<Animated.View style={[styles.collapsibleContainer, bodyFull ? bodyStyle : bodyStyle2]}>
|
||||
<View style={styles.divider} />
|
||||
|
||||
<AppText variant="bodySmall" color={colors.textSecondary} semiBold style={styles.bidLabel as any}>Set Your Bid</AppText>
|
||||
|
||||
{!allBoolean.oneTimeBid && <View style={styles.stepperContainer}>
|
||||
<TouchableOpacity
|
||||
{(!allBoolean.oneTimeBid || config.inlineManualInput) && <View style={styles.stepperContainer}>
|
||||
{!allBoolean.oneTimeBid && <TouchableOpacity
|
||||
onPress={onDecrease}
|
||||
disabled={isMin}
|
||||
style={[styles.stepBtn, isMin && styles.disabledBtn]}
|
||||
disabled={!canDecrease}
|
||||
style={[styles.stepBtn, !canDecrease && styles.disabledBtn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Decrease bid amount"
|
||||
accessibilityState={{ disabled: !canDecrease }}
|
||||
>
|
||||
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>−</AppText>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>}
|
||||
|
||||
<View style={styles.inputBox}>
|
||||
{!allBoolean.oneTimeBid && <View style={styles.inputBox} accessibilityLabel={`Current bid amount ${bidValue} rupees`}>
|
||||
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency as any}>₹</AppText>
|
||||
<AppText variant="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
|
||||
</View>
|
||||
</View>}
|
||||
|
||||
<TouchableOpacity onPress={onIncrease} style={styles.stepBtn}>
|
||||
{config.inlineManualInput && manualInput}
|
||||
|
||||
{!allBoolean.oneTimeBid && <TouchableOpacity
|
||||
onPress={onIncrease}
|
||||
disabled={!canIncrease}
|
||||
style={[styles.stepBtn, !canIncrease && styles.disabledBtn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Increase bid amount"
|
||||
accessibilityState={{ disabled: !canIncrease }}
|
||||
>
|
||||
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>+</AppText>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>}
|
||||
</View>}
|
||||
{
|
||||
(allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}>
|
||||
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency2 as any}>₹</AppText>
|
||||
|
||||
<TextInput
|
||||
value={value}
|
||||
onChangeText={handleChange}
|
||||
keyboardType="decimal-pad"
|
||||
placeholder="0.00"
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
{!config.inlineManualInput && manualInput}
|
||||
|
||||
{(!allBoolean.rebid && item.contractorId == undefined) ? <TouchableOpacity
|
||||
{submitVisible ? <TouchableOpacity
|
||||
style={[styles.submitBtn]}
|
||||
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Confirm bid"
|
||||
accessibilityState={{ busy: allState.isLoading }}
|
||||
>
|
||||
{
|
||||
allState.isLoading ? <Loader size='small' color="#FFF" /> : <AppText variant="button" color="#FFF" bold style={styles.submitBtnText as any}>Confirm Bid</AppText>
|
||||
allState.isLoading ? <Loader size="small" color="#FFF" /> : <AppText variant="button" color="#FFF" bold style={styles.submitBtnText as any}>Confirm Bid</AppText>
|
||||
}
|
||||
</TouchableOpacity> : null}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
:
|
||||
<AuctionItemDetail data={item} onClose={() => { setShowDetail(false) }} visible={showDetail} />
|
||||
<AuctionItemDetail data={item} onClose={() => { setShowDetail(false); }} visible={showDetail} />
|
||||
}
|
||||
|
||||
</View>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(BiddingCard);
|
||||
export default memo(AuctionBiddingCard);
|
||||
|
||||
const makeStyles = (colors: any) => StyleSheet.create({
|
||||
flex1: { flex: 1 },
|
||||
gap10: { gap: 10 },
|
||||
alignEnd: { alignItems: 'flex-end' },
|
||||
inputBox2: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -221,8 +266,13 @@ const makeStyles = (colors: any) => StyleSheet.create({
|
||||
borderRadius: 12,
|
||||
height: 45,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
inputBox2Block: {
|
||||
width: '100%',
|
||||
marginBottom: 20
|
||||
marginBottom: 20,
|
||||
},
|
||||
inputBox2Inline: {
|
||||
flex: 1,
|
||||
},
|
||||
currency2: {
|
||||
marginRight: 4,
|
||||
@@ -231,7 +281,7 @@ const makeStyles = (colors: any) => StyleSheet.create({
|
||||
flex: 1,
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
width: '100%'
|
||||
width: '100%',
|
||||
},
|
||||
card: {
|
||||
borderRadius: 24,
|
||||
@@ -1,65 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import moment from 'moment';
|
||||
import LottieView from 'lottie-react-native';
|
||||
import colors from '../../Constant/Color';
|
||||
|
||||
const AuctionTimer = ({ SelectedAuction, setAllBoolean }: any) => {
|
||||
const [timeLeft, setTimeLeft] = useState('00:00:00');
|
||||
|
||||
useEffect(() => {
|
||||
if (!SelectedAuction?.keyDates?.auctionBidding?.endDateTime) return;
|
||||
|
||||
const format = 'DD-MM-YYYY HH:mm:ss';
|
||||
const endTime = moment(
|
||||
SelectedAuction.keyDates.auctionBidding.endDateTime,
|
||||
format
|
||||
);
|
||||
|
||||
const timer = setInterval(() => {
|
||||
const now = moment();
|
||||
const diff = moment.duration(endTime.diff(now));
|
||||
|
||||
if (diff.asSeconds() <= 0) {
|
||||
setTimeLeft('00:00:00');
|
||||
clearInterval(timer);
|
||||
|
||||
setAllBoolean((prev: any) => ({
|
||||
...prev,
|
||||
timeEnd: true,
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const hours = String(Math.floor(diff.asHours())).padStart(2, '0');
|
||||
const minutes = String(diff.minutes()).padStart(2, '0');
|
||||
const seconds = String(diff.seconds()).padStart(2, '0');
|
||||
|
||||
setTimeLeft(`${hours}:${minutes}:${seconds}`);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [SelectedAuction]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
}}
|
||||
>
|
||||
<LottieView
|
||||
source={require('../../lottie/clock.json')}
|
||||
autoPlay
|
||||
loop
|
||||
style={{ width: 20, height: 20 }}
|
||||
/>
|
||||
<Text style={{ fontSize: 18, color: colors.black }}>{timeLeft}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuctionTimer;
|
||||
@@ -19,7 +19,7 @@ const ConfirmBidModal = ({ visible, onClose, onConfirm, bidAmount, isLoading }:
|
||||
|
||||
return (
|
||||
<View style={styles.overlay}>
|
||||
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.large }]}>
|
||||
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.strong }]}>
|
||||
|
||||
<TouchableOpacity style={[styles.closeBtn, { backgroundColor: colors.background }]} onPress={onClose} disabled={isLoading}>
|
||||
<X size={18} color={colors.textSecondary} strokeWidth={2.5} />
|
||||
|
||||
@@ -19,7 +19,7 @@ const STEPS = [
|
||||
];
|
||||
|
||||
const EmdComplete = ({ onNext }: { onNext?: () => void }) => {
|
||||
const { colors, spacing, shadows } = useTheme();
|
||||
const { colors, shadows } = useTheme();
|
||||
|
||||
return (
|
||||
<View style={[styles.root, { backgroundColor: colors.background }]}>
|
||||
|
||||
@@ -17,7 +17,7 @@ const GoBackModal = ({ visible, onClose, onConfirm }: Props) => {
|
||||
|
||||
return (
|
||||
<View style={styles.overlay}>
|
||||
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.large }]}>
|
||||
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.strong }]}>
|
||||
|
||||
<TouchableOpacity style={[styles.closeBtn, { backgroundColor: colors.background }]} onPress={onClose}>
|
||||
<X size={18} color={colors.textSecondary} strokeWidth={2.5} />
|
||||
|
||||
@@ -1,358 +0,0 @@
|
||||
import React, { memo, useCallback, useRef, useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput } from 'react-native';
|
||||
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
||||
import colors_const from '../../../Constant/Color';
|
||||
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
|
||||
import MainStyle from '../../../Styles/MainStyle';
|
||||
import { useTheme } from '../../../Hooks/useTheme';
|
||||
import AppText from '../../../components/AppText';
|
||||
import axios, { all } from 'axios';
|
||||
import Variables from '../../../Constant/Variable';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
|
||||
import { Loader } from '../../../Component/Index';
|
||||
|
||||
const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => {
|
||||
const { colors } = useTheme();
|
||||
const styles = React.useMemo(() => makeStyles(colors), [colors]);
|
||||
const Token = useSelector((state: any) => state.token.token)
|
||||
const [allState, setAllState] = useState({ isLoading: false })
|
||||
const [showDetail, setShowDetail] = useState(false);
|
||||
const [value, setValue] = useState(`${item.bidAmount}`);
|
||||
const canDecrease = bidValue > item.emdAmount;
|
||||
const canIncrease = bidValue < item.bidAmount;
|
||||
// Animation Part Rohit Start
|
||||
|
||||
const expandedRef = useRef(false);
|
||||
const animation = useSharedValue(0);
|
||||
|
||||
const toggleExpand = useCallback(() => {
|
||||
expandedRef.current = !expandedRef.current;
|
||||
animation.value = withSpring(expandedRef.current ? 1 : 0, {
|
||||
damping: 15,
|
||||
stiffness: 140,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const bodyStyle = useAnimatedStyle(() => ({
|
||||
height: interpolate(animation.value, [0, 1], [0, 190]),
|
||||
opacity: interpolate(animation.value, [0, 0.5, 1], [0, 0, 1]),
|
||||
marginTop: interpolate(animation.value, [0, 1], [0, 15]),
|
||||
}));
|
||||
const bodyStyle2 = useAnimatedStyle(() => ({
|
||||
height: interpolate(animation.value, [0, 1], [0, 100]),
|
||||
opacity: interpolate(animation.value, [0, 0.5, 1], [0, 0, 1]),
|
||||
marginTop: interpolate(animation.value, [0, 1], [0, 15]),
|
||||
}));
|
||||
|
||||
const arrowStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
rotate: `${interpolate(animation.value, [0, 1], [0, 180])}deg`,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// Animation Part Rohit End
|
||||
|
||||
const MAX_VALUE = Number(item.auctionStartValue);
|
||||
|
||||
const handleChange = (text: string) => {
|
||||
let cleaned = text.replace(/[^0-9.]/g, '');
|
||||
if ((cleaned.match(/\./g) || []).length > 1) return;
|
||||
if (cleaned === '') {
|
||||
setValue('');
|
||||
return;
|
||||
}
|
||||
const numericValue = Number(cleaned);
|
||||
if (numericValue < 0) return;
|
||||
if (numericValue > MAX_VALUE) return;
|
||||
setValue(cleaned);
|
||||
};
|
||||
|
||||
const oneTimePlaceBid = async () => {
|
||||
try {
|
||||
setAllState((prev: any) => ({ ...prev, isLoading: true }))
|
||||
const payload = {
|
||||
itemId: item.id,
|
||||
bidAmount: Number(value),
|
||||
auctionId: SelectedAuction.auctionId,
|
||||
contractorId: Token.contractorId,
|
||||
fullName: Token.fullName,
|
||||
};
|
||||
|
||||
|
||||
const res = await axios.post(
|
||||
`${Variables.socketUrl}bidding/placeBidForAuction`,
|
||||
[payload],
|
||||
{ headers: { Authentication: `Bearer ${Token.token}` } }
|
||||
);
|
||||
|
||||
if (res.data.statusCode === 200) {
|
||||
showSuccessAlert(res.data.message);
|
||||
}
|
||||
setAllBoolean((prev: any) => ({ ...prev, reload: !prev.reload }))
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
showErrorAlert(error.response?.data.message)
|
||||
} else {
|
||||
console.log('UNKNOWN ERROR:', error);
|
||||
}
|
||||
|
||||
} finally {
|
||||
setAllState((prev: any) => ({ ...prev, isLoading: false }))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{!showDetail ? (
|
||||
<Animated.View
|
||||
layout={Layout.springify()}
|
||||
entering={FadeInUp.delay(100)}
|
||||
style={[
|
||||
styles.card,
|
||||
item.me ? styles.leadingCard : undefined,
|
||||
{
|
||||
borderColor: item.me ? colors.success : colors.error,
|
||||
backgroundColor: item.me ? `${colors.success}10` : `${colors.error}10`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* HEADER */}
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={() => setShowDetail(true)}
|
||||
style={styles.cardHeader}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<AppText variant="h4" color={colors.text} bold style={styles.productName as any}>{item.productName}</AppText>
|
||||
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
|
||||
Initial Amt : ₹{item.auctionStartValue}
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
|
||||
<View style={[MainStyle.flexBetween, { gap: 10 }]}>
|
||||
<View
|
||||
style={[
|
||||
styles.badge,
|
||||
item.me ? styles.meBadge : styles.liveBadge,
|
||||
]}
|
||||
>
|
||||
<AppText
|
||||
variant="caption"
|
||||
bold
|
||||
style={[
|
||||
styles.badgeText as any,
|
||||
item.me ? styles.meText : styles.liveText,
|
||||
]}
|
||||
>
|
||||
{item.me ? 'LEADING' : 'LIVE'}
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
<Animated.Text style={[styles.arrow, { color: colors.textSecondary }, arrowStyle]}>
|
||||
▼
|
||||
</Animated.Text>
|
||||
</View>
|
||||
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
|
||||
{SelectedAuction?.auctionNumber}
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* PRICE INFO */}
|
||||
<View style={styles.priceRow}>
|
||||
<View>
|
||||
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Current Bid</AppText>
|
||||
<AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item.bidAmount || item.auctionStartValue}</AppText>
|
||||
</View>
|
||||
|
||||
<View style={{ alignItems: 'flex-end' }}>
|
||||
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Decrement</AppText>
|
||||
<AppText variant="body" color={colors.primary} bold style={styles.incrementText as any}>
|
||||
-₹{item.emdAmount}
|
||||
</AppText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* COLLAPSIBLE BODY */}
|
||||
{/* <Animated.View style={[styles.collapsibleContainer, (allBoolean.oneTimeBid && allBoolean.rebid) ? bodyStyle : (allBoolean.oneTimeBid && !item.me) ? bodyStyle : bodyStyle2]}> */}
|
||||
<Animated.View style={[styles.collapsibleContainer, bodyStyle]}>
|
||||
<View style={styles.divider} />
|
||||
|
||||
<AppText variant="bodySmall" color={colors.textSecondary} semiBold style={styles.bidLabel as any}>Set Your Bid</AppText>
|
||||
|
||||
<View style={styles.stepperContainer}>
|
||||
{/* DECREASE */}
|
||||
{!allBoolean.oneTimeBid && <TouchableOpacity
|
||||
onPress={onDecrease}
|
||||
disabled={!canDecrease}
|
||||
style={[
|
||||
styles.stepBtn,
|
||||
!canDecrease && styles.disabledBtn,
|
||||
]}
|
||||
>
|
||||
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>−</AppText>
|
||||
</TouchableOpacity>}
|
||||
|
||||
{/* VALUE */}
|
||||
{!allBoolean.oneTimeBid && <View style={styles.inputBox}>
|
||||
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency as any}>₹</AppText>
|
||||
<AppText variant="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
|
||||
</View>}
|
||||
{
|
||||
(allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}>
|
||||
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency2 as any}>₹</AppText>
|
||||
|
||||
<TextInput
|
||||
value={value}
|
||||
onChangeText={handleChange}
|
||||
keyboardType="decimal-pad"
|
||||
placeholder="0.00"
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
|
||||
{/* INCREASE */}
|
||||
{!allBoolean.oneTimeBid && <TouchableOpacity
|
||||
onPress={onIncrease}
|
||||
disabled={!canIncrease}
|
||||
style={[
|
||||
styles.stepBtn,
|
||||
!canIncrease && styles.disabledBtn,
|
||||
]}
|
||||
>
|
||||
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>+</AppText>
|
||||
</TouchableOpacity>}
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.submitBtn]}
|
||||
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
|
||||
>
|
||||
{
|
||||
allState.isLoading ? <Loader size='small' color="#FFF" /> : <AppText variant="button" color="#FFF" bold style={styles.submitBtnText as any}>Confirm Bid</AppText>
|
||||
}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* {(allBoolean.oneTimeBid && allBoolean.rebid) ?
|
||||
<TouchableOpacity
|
||||
style={[styles.submitBtn]}
|
||||
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
|
||||
>
|
||||
{
|
||||
allState.isLoading ? <Loader size='small' color={colors.white} /> : <Text style={styles.submitBtnText}>Confirm Bid</Text>
|
||||
}
|
||||
</TouchableOpacity> :
|
||||
(allBoolean.oneTimeBid && !item.me) ? <TouchableOpacity
|
||||
style={[styles.submitBtn]}
|
||||
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
|
||||
>
|
||||
{
|
||||
allState.isLoading ? <Loader size='small' color={colors.white} /> : <Text style={styles.submitBtnText}>Confirm Bid</Text>
|
||||
}
|
||||
</TouchableOpacity> : null
|
||||
} */}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
) : (
|
||||
<AuctionItemDetail
|
||||
data={item}
|
||||
onClose={() => setShowDetail(false)}
|
||||
visible={showDetail}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReverseBiddingCard;
|
||||
|
||||
const makeStyles = (colors: any) => StyleSheet.create({
|
||||
inputBox2: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.background,
|
||||
borderRadius: 12,
|
||||
height: 45,
|
||||
paddingHorizontal: 12,
|
||||
width: '100%'
|
||||
},
|
||||
currency2: {
|
||||
marginRight: 4,
|
||||
} as any,
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
width: '100%'
|
||||
},
|
||||
card: {
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
borderWidth: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
leadingCard: {
|
||||
borderWidth: 2,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 },
|
||||
productName: {} as any,
|
||||
itemIdText: { marginTop: 2 } as any,
|
||||
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
||||
liveBadge: { backgroundColor: `${colors.error}20` },
|
||||
meBadge: { backgroundColor: `${colors.success}20` },
|
||||
badgeText: { fontSize: 10 } as any,
|
||||
liveText: { color: colors.error },
|
||||
meText: { color: colors.success },
|
||||
arrow: { fontSize: 10 },
|
||||
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
|
||||
label: {
|
||||
textTransform: 'uppercase',
|
||||
} as any,
|
||||
mainPrice: {} as any,
|
||||
incrementText: {} as any,
|
||||
collapsibleContainer: { overflow: 'hidden' },
|
||||
divider: { height: 1, backgroundColor: colors.border, marginVertical: 10 },
|
||||
bidLabel: { marginBottom: 10 } as any,
|
||||
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
|
||||
stepBtn: {
|
||||
width: 45,
|
||||
height: 45,
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 12,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
disabledBtn: { backgroundColor: colors.border },
|
||||
stepText: {} as any,
|
||||
inputBox: {
|
||||
flex: 1,
|
||||
height: 45,
|
||||
backgroundColor: colors.background,
|
||||
borderRadius: 12,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
currency: { marginRight: 4 } as any,
|
||||
bidValueText: {} as any,
|
||||
submitBtn: {
|
||||
height: 50,
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 15,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
submitBtnText: {} as any,
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import { useSelector } from 'react-redux'
|
||||
import { apiCall } from '../../../Axios/Axios'
|
||||
import { TECHNICAL_DOCUMENT_SUBMIT, TECHNICAL_VALUE_SUBMIT } from '../../../Services/ApiCall/ApiUrls'
|
||||
import { Props } from '../Type/BidType'
|
||||
import { Loader } from '../../../Component/Index'
|
||||
import { Loader } from '../../../components'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import NavigationString from '../../../Constant/NavigationString'
|
||||
import { useTheme } from '../../../Hooks/useTheme'
|
||||
@@ -15,13 +15,16 @@ import AppText from '../../../components/AppText'
|
||||
import { Upload, FileText, X } from 'lucide-react-native'
|
||||
|
||||
const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }) => {
|
||||
if (!template) return null
|
||||
|
||||
const SelectedAuction = useSelector((state: any) => state.auction.auction)
|
||||
const Navigation: any = useNavigation()
|
||||
const { colors, spacing, typography } = useTheme()
|
||||
const styles = useMemo(() => makeStyles(colors, spacing, typography), [colors, spacing, typography])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// Hooks must all run before this guard — a conditional return above any
|
||||
// hook changes the hook order between renders and crashes React.
|
||||
if (!template) return null
|
||||
|
||||
const initialValues = template?.labels?.reduce((acc: any, label: any) => {
|
||||
acc[label.labelName] = ''
|
||||
return acc
|
||||
@@ -254,7 +257,7 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
|
||||
})
|
||||
}}
|
||||
>
|
||||
{({ handleSubmit, values, errors, touched, setFieldValue }) => (
|
||||
{({ handleSubmit, values, setFieldValue }) => (
|
||||
<>
|
||||
{template.labels.map((label: any) =>
|
||||
renderInput(
|
||||
@@ -298,7 +301,7 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
|
||||
|
||||
export default TemplateFormModal
|
||||
|
||||
const makeStyles = (colors: any, spacing: any, typography: any) =>
|
||||
const makeStyles = (colors: any, spacing: any, _typography: any) =>
|
||||
StyleSheet.create({
|
||||
screen: {
|
||||
flex: 1,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { pick } from '@react-native-documents/picker';
|
||||
import Animated, { FadeInDown, FadeOutUp, Layout } from 'react-native-reanimated';
|
||||
import BouncyCheckbox from 'react-native-bouncy-checkbox';
|
||||
import { InsideHeader, Skelton } from '../../Component/Index';
|
||||
import { InsideHeader, Skeleton } from '../../components';
|
||||
import EmdComplete from './Component/EmdComplete';
|
||||
import NavigationString from '../../Constant/NavigationString';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
@@ -34,12 +34,9 @@ import AppText from '../../components/AppText';
|
||||
import {
|
||||
FileText,
|
||||
Upload,
|
||||
CheckCircle2,
|
||||
ShieldCheck,
|
||||
Paperclip,
|
||||
ChevronRight,
|
||||
PackageCheck,
|
||||
BadgeIndianRupee,
|
||||
} from 'lucide-react-native';
|
||||
|
||||
const Emd = () => {
|
||||
@@ -350,6 +347,9 @@ const Emd = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Run once on mount; AlreadyEmdCheck is redeclared each render, so listing
|
||||
// it as a dependency would re-fire the check on every render.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => { AlreadyEmdCheck(); }, []);
|
||||
|
||||
const styles = makeStyles(colors, spacing, shadows, typography);
|
||||
@@ -410,7 +410,7 @@ const Emd = () => {
|
||||
)}
|
||||
|
||||
{(isPending || isInitialLoading) ? (
|
||||
<Skelton />
|
||||
<Skeleton />
|
||||
) : allBoolean.isEmdComplete ? (
|
||||
<EmdComplete onNext={EmdContinueHandle} />
|
||||
) : allBoolean.emdType ? (
|
||||
|
||||
@@ -1,349 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { View, FlatList, StyleSheet, Platform, Alert, BackHandler, Text, ToastAndroid } from 'react-native';
|
||||
import LottieView from 'lottie-react-native';
|
||||
import { InsideHeader } from '../../Component/Index';
|
||||
import colors from '../../Constant/Color';
|
||||
import { useWebSocketService } from '../../socket/Socket';
|
||||
import Variables from '../../Constant/Variable';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { apiCall } from '../../Axios/Axios';
|
||||
import { AUCTION_ONE_TIME_BID, GET_AUCTION_DETAILS, REVERSE_AUCTION_ITEM, SELECTED_AUCTION_ITEM } from '../../Services/ApiCall/ApiUrls';
|
||||
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||
import NavigationString from '../../Constant/NavigationString';
|
||||
import CountdownTimer from '../../Component/Counter/CountdownTimer';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
import ReverseBiddingCard from './Component/ReverseBiddingCard';
|
||||
import ConfirmBidModal from './Component/ConfirmBidModal';
|
||||
import GoBackModal from './Component/GoBackModal';
|
||||
|
||||
const StartReverseBidding = () => {
|
||||
const { colors } = useTheme();
|
||||
const styles = React.useMemo(() => makeStyles(colors), [colors]);
|
||||
const SelectedAuction = useSelector((state: any) => state.auction.auction);
|
||||
const loginUser = useSelector((state: any) => state.token.token)
|
||||
const Navigation: any = useNavigation()
|
||||
const [items, setItems] = useState<any>([]);
|
||||
const [bidValues, setBidValues] = useState<Record<number, number>>({});
|
||||
const [allBoolean, setAllBoolean] = useState({ showSuccess: false, showDialog: false, showBackDialog: false, bidPayload: {} as any, oneTimeBid: false, rebid: false, reload: false })
|
||||
const { subscribe, createRoom, placeBid } = useWebSocketService(
|
||||
Variables.webSocketUrl,
|
||||
() => console.log('Connected!'),
|
||||
(error) => console.log('WebSocket Error:', error),
|
||||
loginUser.token,
|
||||
);
|
||||
const getAuction = async () => {
|
||||
const selectedRes = await apiCall<any>('get', GET_AUCTION_DETAILS, {}, { id: SelectedAuction.auctionId });
|
||||
if (selectedRes.statusCode == 200 && selectedRes.data.contractorId.length) {
|
||||
setAllBoolean((prev: any) => ({ ...prev, oneTimeBid: true }))
|
||||
}
|
||||
}
|
||||
|
||||
const getOneTimeBidValue = async () => {
|
||||
try {
|
||||
const selectedRes = await apiCall<any>('get', AUCTION_ONE_TIME_BID, {}, { auctionId: SelectedAuction.auctionId });
|
||||
if (selectedRes.statusCode == 200) {
|
||||
return selectedRes.data
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const getAllAcutionItems = async () => {
|
||||
try {
|
||||
const payload = { auctionNumber: SelectedAuction.auctionNumber };
|
||||
let auctionItem: any[] = [];
|
||||
|
||||
if (SelectedAuction.auctionPattern === 'Reverse') {
|
||||
const res = await apiCall<any>('get', REVERSE_AUCTION_ITEM, {}, payload);
|
||||
if (res?.statusCode === 200) auctionItem = res.data || [];
|
||||
}
|
||||
|
||||
const selectedRes = await apiCall<any>('get', SELECTED_AUCTION_ITEM, {}, payload);
|
||||
|
||||
const uniqueArray: number[] = Array.from(
|
||||
new Set(selectedRes?.data || [])
|
||||
);
|
||||
|
||||
const filteredItems = auctionItem.filter(item =>
|
||||
uniqueArray.includes(item.id)
|
||||
);
|
||||
|
||||
if (SelectedAuction.auctionCategories == 'Live') {
|
||||
const result = await createRoom(SelectedAuction.auctionNumber);
|
||||
|
||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, data => {
|
||||
console.log('🔥 BID UPDATE:', data);
|
||||
});
|
||||
const bidAmountMap2 = new Map<number, { bidAmount: number; contractorId: number }>();
|
||||
|
||||
(result || []).forEach((r: any) => {
|
||||
bidAmountMap2.set(r.auctionItemId, {
|
||||
bidAmount: r.bidAmount,
|
||||
contractorId: r.contractorId,
|
||||
});
|
||||
});
|
||||
// Check One Time bid enable butoon
|
||||
if (SelectedAuction.auctionCategories != 'Live') {
|
||||
const myBid = result.find(
|
||||
(bid: any) => bid.contractorId === loginUser.contractorId
|
||||
);
|
||||
|
||||
if (myBid) {
|
||||
setAllBoolean((prev: any) => ({ ...prev, oneTimeBid: true }))
|
||||
}
|
||||
}
|
||||
|
||||
const bidAmountMap = new Map<number, number>();
|
||||
|
||||
(result || []).forEach((r: any) => {
|
||||
bidAmountMap.set(r.auctionItemId, r.bidAmount);
|
||||
});
|
||||
|
||||
const updatedFilteredItems = filteredItems.map(item => {
|
||||
const bidData = bidAmountMap2.get(item.id);
|
||||
return {
|
||||
...item,
|
||||
bidAmount: bidData?.bidAmount ?? item.auctionStartValue,
|
||||
bidAvilable: bidData?.bidAmount ?? 0,
|
||||
me: bidData?.contractorId == loginUser.contractorId ? true : false
|
||||
};
|
||||
});
|
||||
|
||||
const initialBids: Record<number, number> = {};
|
||||
updatedFilteredItems.forEach(item => {
|
||||
initialBids[item.id] = item.bidAmount;
|
||||
});
|
||||
setBidValues(initialBids);
|
||||
setItems(updatedFilteredItems);
|
||||
} else {
|
||||
let result = []
|
||||
if (SelectedAuction.rebidAllowed) {
|
||||
setAllBoolean((prev: any) => ({ ...prev, rebid: true }))
|
||||
}
|
||||
|
||||
if (SelectedAuction.auctionCategories != 'Live') {
|
||||
result = await getOneTimeBidValue()
|
||||
}
|
||||
const bidMap = new Map<number, any>();
|
||||
|
||||
result.forEach((bid: any) => {
|
||||
bidMap.set(bid.auctionItemId, bid);
|
||||
});
|
||||
const mergedData = auctionItem.map((item: any) => {
|
||||
const bid = bidMap.get(item.id);
|
||||
|
||||
if (!bid) {
|
||||
return {
|
||||
...item,
|
||||
bidAmount: item.auctionStartValue,
|
||||
contractorId: item.contractorId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
bidAmount: bid.bidAmount,
|
||||
bidId: bid.bidId,
|
||||
contractorId: bid.contractorId,
|
||||
contractorName: bid.contractorName,
|
||||
me: bid.contractorId === loginUser.contractorId,
|
||||
bidTime: bid.bidTime,
|
||||
};
|
||||
});
|
||||
const initialBids: Record<number, number> = {};
|
||||
mergedData.forEach(item => {
|
||||
initialBids[item.id] = item.bidAmount;
|
||||
});
|
||||
setBidValues(initialBids);
|
||||
setItems(mergedData)
|
||||
setBidValues(initialBids);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching auction items:', error);
|
||||
}
|
||||
};
|
||||
|
||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, async (message) => {
|
||||
|
||||
const result = message[message.length - 1]
|
||||
|
||||
const updatedBidItem = (prev: any[]) =>
|
||||
prev.map(item =>
|
||||
item.id === result.auctionItemId
|
||||
? { ...item, bidAmount: result.bidAmount, me: result.contractorId == loginUser.contractorId ? true : false }
|
||||
: item
|
||||
);
|
||||
|
||||
setItems(updatedBidItem);
|
||||
|
||||
setBidValues(prev => ({
|
||||
...prev,
|
||||
[result.auctionItemId]: result.bidAmount,
|
||||
}));
|
||||
try {
|
||||
|
||||
} catch (err) {
|
||||
console.log('JSON parse error:', err);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (SelectedAuction.auctionCategories == 'One-time') {
|
||||
getAuction()
|
||||
}
|
||||
getAllAcutionItems();
|
||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, (data) => { });
|
||||
}, [allBoolean.reload]);
|
||||
|
||||
useEffect(() => {
|
||||
const initial: Record<number, number> = {};
|
||||
items.forEach((item: any) => {
|
||||
initial[item.id] = item.bidAmount + item.bidVariationValue;
|
||||
});
|
||||
setBidValues(initial);
|
||||
}, []);
|
||||
|
||||
const handleUpdate = (id: number, val: number) => {
|
||||
setBidValues(prev => ({ ...prev, [id]: prev[id] + val }));
|
||||
};
|
||||
|
||||
const submitBid = async (item: any) => {
|
||||
const payload = {
|
||||
itemId: item.id,
|
||||
bidAmount: bidValues[item.id],
|
||||
auctionId: SelectedAuction.auctionId,
|
||||
contractorId: loginUser.contractorId,
|
||||
fullName: loginUser.fullName,
|
||||
roomId: SelectedAuction.auctionNumber,
|
||||
};
|
||||
setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, showDialog: true }));
|
||||
};
|
||||
|
||||
const placeBidSubmit = async (data: any) => {
|
||||
setAllBoolean((prev: any) => ({ ...prev, showDialog: false }));
|
||||
try {
|
||||
const result: any = await placeBid(data);
|
||||
|
||||
if (result?.status && result.data) {
|
||||
const updatedBidItem = (prev: any[]) =>
|
||||
prev.map(item =>
|
||||
item.id === result.data.auctionItemId
|
||||
? { ...item, bidAmount: result.data.bidAmount, me: result.data?.contractorId == loginUser.contractorId ? true : false }
|
||||
: item
|
||||
);
|
||||
|
||||
getAllAcutionItems()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error placing bid:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const onBackPress = () => {
|
||||
setAllBoolean((prev: any) => ({ ...prev, showBackDialog: true }));
|
||||
return true; // block default back action
|
||||
};
|
||||
|
||||
const subscription = BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
onBackPress
|
||||
);
|
||||
|
||||
return () => subscription.remove();
|
||||
}, [Navigation])
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<InsideHeader title="Bidding Panel" showArrow />
|
||||
<View style={styles.container2}>
|
||||
<AppText variant="h3" color={colors.text} bold style={styles.title as any}>
|
||||
{SelectedAuction.deptName}
|
||||
</AppText>
|
||||
|
||||
<View style={styles.timerWrapper}>
|
||||
<CountdownTimer
|
||||
endDate={SelectedAuction.keyDates.auctionBidding.endDateTime}
|
||||
myStyle={{ fontSize: 30 }}
|
||||
size={30}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={items}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
renderItem={({ item, index }) => (
|
||||
<ReverseBiddingCard
|
||||
item={{ ...item, index }}
|
||||
bidValue={bidValues[item.id] || 0}
|
||||
onIncrease={() => handleUpdate(item.id, item.emdAmount)}
|
||||
onDecrease={() => handleUpdate(item.id, -item.emdAmount)}
|
||||
onPlaceBid={() => { submitBid(item) }}
|
||||
SelectedAuction={SelectedAuction}
|
||||
allBoolean={allBoolean}
|
||||
setAllBoolean={setAllBoolean}
|
||||
/>
|
||||
)}
|
||||
contentContainerStyle={{ padding: 16 }}
|
||||
/>
|
||||
|
||||
{allBoolean.showSuccess && (
|
||||
<View style={styles.overlay}>
|
||||
<LottieView
|
||||
source={require('../../lottie/Success.json')}
|
||||
autoPlay
|
||||
loop={false}
|
||||
style={{ width: 200, height: 200 }}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<ConfirmBidModal
|
||||
visible={allBoolean.showDialog}
|
||||
onClose={() => setAllBoolean(prev => ({ ...prev, showDialog: false }))}
|
||||
onConfirm={() => placeBidSubmit(allBoolean.bidPayload)}
|
||||
bidAmount={allBoolean.bidPayload?.bidAmount}
|
||||
/>
|
||||
|
||||
<GoBackModal
|
||||
visible={allBoolean.showBackDialog}
|
||||
onClose={() => setAllBoolean(prev => ({ ...prev, showBackDialog: false }))}
|
||||
onConfirm={() => {
|
||||
setAllBoolean(prev => ({ ...prev, showBackDialog: false }));
|
||||
Navigation.dispatch(
|
||||
CommonActions.reset({
|
||||
index: 0,
|
||||
routes: [{ name: NavigationString.Home }],
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default StartReverseBidding;
|
||||
|
||||
const makeStyles = (colors: any) => StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: colors.background },
|
||||
container2: {
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
title: {
|
||||
marginBottom: 6,
|
||||
} as any,
|
||||
timerWrapper: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: colors.overlay, justifyContent: 'center', alignItems: 'center', zIndex: 99 },
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { getBiddingConfig } from '../biddingPattern';
|
||||
import { FORWARD_AUCTION_ITEM, REVERSE_AUCTION_ITEM } from '../../../Services/ApiCall/ApiUrls';
|
||||
|
||||
describe('getBiddingConfig', () => {
|
||||
it('returns the reverse config only for exactly "Reverse"', () => {
|
||||
expect(getBiddingConfig('Reverse').pattern).toBe('Reverse');
|
||||
// Forward is the safe default for anything else (Forward, undefined, junk).
|
||||
expect(getBiddingConfig('Forward').pattern).toBe('Forward');
|
||||
expect(getBiddingConfig(undefined).pattern).toBe('Forward');
|
||||
expect(getBiddingConfig('anything').pattern).toBe('Forward');
|
||||
});
|
||||
});
|
||||
|
||||
describe('forward pattern', () => {
|
||||
const cfg = getBiddingConfig('Forward');
|
||||
const item = { auctionStartValueFigure: 100, bidVariationValue: 10, bidAmount: 150 };
|
||||
|
||||
it('reads the forward start-value and step fields', () => {
|
||||
expect(cfg.itemsUrl).toBe(FORWARD_AUCTION_ITEM);
|
||||
expect(cfg.startValue(item)).toBe(100);
|
||||
expect(cfg.step(item)).toBe(10);
|
||||
expect(cfg.stepLabel).toBe('Increment');
|
||||
expect(cfg.stepPrefix).toBe('+');
|
||||
expect(cfg.inlineManualInput).toBe(false);
|
||||
});
|
||||
|
||||
it('allows unbounded increases but blocks decreasing below the current bid', () => {
|
||||
expect(cfg.canIncrease(999999, item)).toBe(true);
|
||||
expect(cfg.canDecrease(160, item)).toBe(true); // 160 > 150
|
||||
expect(cfg.canDecrease(150, item)).toBe(false); // not strictly greater
|
||||
expect(cfg.canDecrease(140, item)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a manual bid below the current bid', () => {
|
||||
expect(cfg.validateManualBid(149, item)).toBe('Enter a value greater than the current bid value');
|
||||
expect(cfg.validateManualBid(150, item)).toBeNull();
|
||||
expect(cfg.validateManualBid(200, item)).toBeNull();
|
||||
});
|
||||
|
||||
it('accepts any typed input (forward is unbounded)', () => {
|
||||
expect(cfg.acceptManualInput(5, item)).toBe(true);
|
||||
});
|
||||
|
||||
it('shows body/submit only for a fresh (un-bid) item when not rebidding', () => {
|
||||
const fresh = { contractorId: undefined };
|
||||
const alreadyBid = { contractorId: 7 };
|
||||
expect(cfg.isBodyFull({ oneTimeBid: false, rebid: false }, fresh)).toBe(true);
|
||||
expect(cfg.showSubmit({ oneTimeBid: false, rebid: false }, fresh)).toBe(true);
|
||||
expect(cfg.showSubmit({ oneTimeBid: false, rebid: false }, alreadyBid)).toBe(false);
|
||||
expect(cfg.showSubmit({ oneTimeBid: false, rebid: true }, fresh)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reverse pattern', () => {
|
||||
const cfg = getBiddingConfig('Reverse');
|
||||
const item = { auctionStartValue: 1000, emdAmount: 50, bidAmount: 800 };
|
||||
|
||||
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.stepLabel).toBe('Decrement');
|
||||
expect(cfg.stepPrefix).toBe('-');
|
||||
expect(cfg.inlineManualInput).toBe(true);
|
||||
});
|
||||
|
||||
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);
|
||||
expect(cfg.canDecrease(51, item)).toBe(true); // 51 > 50
|
||||
expect(cfg.canDecrease(50, item)).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts typed input only within [0, auctionStartValue]', () => {
|
||||
expect(cfg.acceptManualInput(0, item)).toBe(true);
|
||||
expect(cfg.acceptManualInput(1000, item)).toBe(true);
|
||||
expect(cfg.acceptManualInput(1001, item)).toBe(false);
|
||||
expect(cfg.acceptManualInput(-1, item)).toBe(false);
|
||||
});
|
||||
|
||||
it('always shows body + submit (reverse has no fresh-item gate)', () => {
|
||||
expect(cfg.isBodyFull({ oneTimeBid: true, rebid: true }, item)).toBe(true);
|
||||
expect(cfg.showSubmit({ oneTimeBid: false, rebid: false }, item)).toBe(true);
|
||||
expect(cfg.validateManualBid(123, item)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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. */
|
||||
step: (item: any) => number;
|
||||
stepLabel: 'Increment' | 'Decrement';
|
||||
stepPrefix: '+' | '-';
|
||||
/** Forward bids may rise without bound; reverse bids may not exceed the current bid. */
|
||||
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;
|
||||
/** 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;
|
||||
};
|
||||
|
||||
const forward: BiddingPatternConfig = {
|
||||
pattern: 'Forward',
|
||||
itemsUrl: FORWARD_AUCTION_ITEM,
|
||||
startValue: item => item?.auctionStartValueFigure,
|
||||
step: item => item?.bidVariationValue,
|
||||
stepLabel: 'Increment',
|
||||
stepPrefix: '+',
|
||||
canIncrease: () => true,
|
||||
canDecrease: (bidValue, item) => bidValue > item.bidAmount,
|
||||
acceptManualInput: () => true,
|
||||
validateManualBid: (value, item) =>
|
||||
value < Number(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 => item?.auctionStartValue,
|
||||
step: item => item?.emdAmount,
|
||||
stepLabel: 'Decrement',
|
||||
stepPrefix: '-',
|
||||
canIncrease: (bidValue, item) => bidValue < item.bidAmount,
|
||||
canDecrease: (bidValue, item) => bidValue > item.emdAmount,
|
||||
acceptManualInput: (value, item) => value >= 0 && value <= Number(item?.auctionStartValue),
|
||||
validateManualBid: () => null,
|
||||
inlineManualInput: true,
|
||||
isBodyFull: () => true,
|
||||
showSubmit: () => true,
|
||||
};
|
||||
|
||||
export const getBiddingConfig = (pattern: string | undefined): BiddingPatternConfig =>
|
||||
pattern === 'Reverse' ? reverse : forward;
|
||||
Reference in New Issue
Block a user