feat: architecture refactor — core layer, spreadsheet engine, envelope bidding, tests
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:
Abin Toppo
2026-07-16 10:20:22 +05:30
co-authored by Claude Fable 5
parent 9acec328a7
commit 1d7646d938
217 changed files with 11068 additions and 6855 deletions
@@ -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 },
});
@@ -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,
+5 -5
View File
@@ -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 ? (
-349
View File
@@ -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();
});
});
+78
View File
@@ -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;