import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { View, FlatList, StyleSheet, BackHandler, } from 'react-native'; import LottieView from 'lottie-react-native'; import Animated, { FadeInDown } from 'react-native-reanimated'; 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, GET_AUCTION_DETAILS, SELECTED_AUCTION_ITEM, } from '../../Services/ApiCall/ApiUrls'; 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 '../../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'; /** * 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([]); const [bidValues, setBidValues] = useState>({}); const [allBoolean, setAllBoolean] = useState({ showSuccess: false, showDialog: false, showBackDialog: false, bidPayload: {} as any, oneTimeBid: false, rebid: false, reload: false, }); const { subscribe, unsubscribe, disconnect, createRoom, placeBid } = useWebSocketService( Variables.webSocketUrl, () => logger.info('stomp', 'connected'), (error: any) => logger.warn('stomp', `connection error: ${error}`), ); const getAuction = async () => { const selectedRes = await apiCall('get', GET_AUCTION_DETAILS, {}, { id: SelectedAuction.auctionId }); if (selectedRes.statusCode === 200 && selectedRes.data.contractorId.length) { setAllBoolean(prev => ({ ...prev, oneTimeBid: true })); } }; const getOneTimeBidValue = async () => { try { const selectedRes = await apiCall('get', AUCTION_ONE_TIME_BID, {}, { auctionId: SelectedAuction.auctionId }); return selectedRes.statusCode === 200 ? selectedRes.data : []; } catch { return []; } }; const getAllAcutionItems = async () => { try { const payload = { auctionNumber: SelectedAuction.auctionNumber }; let auctionItem: any[] = []; const res = await apiCall('get', config.itemsUrl, {}, payload); if (res?.statusCode === 200) { auctionItem = res.data || []; } const selectedRes = await apiCall('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); const liveBids = new Map(); (result || []).forEach((r: any) => { liveBids.set(r.auctionItemId, { bidAmount: r.bidAmount, contractorId: r.contractorId }); }); const updatedFilteredItems = filteredItems.map(item => { const bidData = liveBids.get(item.id); return { ...item, bidAmount: bidData?.bidAmount ?? config.startValue(item), bidAvilable: bidData?.bidAmount ?? 0, me: bidData?.contractorId === loginUser.contractorId, }; }); const initialBids: Record = {}; updatedFilteredItems.forEach(item => { initialBids[item.id] = item.bidAmount; }); setBidValues(initialBids); setItems(updatedFilteredItems); } else { if (SelectedAuction.rebidAllowed) { setAllBoolean(prev => ({ ...prev, rebid: true })); } const result: any[] = await getOneTimeBidValue(); const bidMap = new Map(); 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: config.startValue(item), 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 = {}; mergedData.forEach(item => { initialBids[item.id] = item.bidAmount; }); setBidValues(initialBids); setItems(mergedData); } } catch (error) { logger.error('bidding', 'fetching auction items failed', { error: String(error) }); } }; // 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(); } getAllAcutionItems(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [allBoolean.reload]); 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 => ({ ...prev, bidPayload: payload, showDialog: true })); }; const placeBidSubmit = async (data: any) => { setAllBoolean(prev => ({ ...prev, showDialog: false })); try { 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) { logger.error('bidding', 'placing bid failed', { error: String(error) }); } }; useFocusEffect( useCallback(() => { const onBackPress = () => { setAllBoolean(prev => ({ ...prev, showBackDialog: true })); return true; }; const sub = BackHandler.addEventListener('hardwareBackPress', onBackPress); return () => sub.remove(); }, []), ); const styles = makeStyles(colors, spacing, shadows); const biddingEndDate = SelectedAuction?.auctionStatus === 'Amended' ? SelectedAuction?.amendedkeyDates?.auctionBidding?.endDateTime : SelectedAuction?.keyDates?.auctionBidding?.endDateTime; return ( {/* ── Top Info Band ── */} {/* Department row */} {SelectedAuction.deptName ?? 'Auction Department'} {/* Timer card */} BIDDING ENDS IN {/* Auction chip */} {SelectedAuction.auctionNumber} {SelectedAuction.auctionCategories ?? 'Auction'} {/* ── Items List ── */} item.id.toString()} renderItem={({ item, index }) => ( 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} showsVerticalScrollIndicator={false} /> {/* ── Success overlay ── */} {allBoolean.showSuccess && ( )} setAllBoolean(prev => ({ ...prev, showDialog: false }))} onConfirm={() => placeBidSubmit(allBoolean.bidPayload)} bidAmount={allBoolean.bidPayload?.bidAmount} /> setAllBoolean(prev => ({ ...prev, showBackDialog: false }))} onConfirm={() => { setAllBoolean(prev => ({ ...prev, showBackDialog: false })); Navigation.dispatch( CommonActions.reset({ index: 0, routes: [{ name: NavigationString.Home }], }) ); }} /> ); }; export default AuctionBiddingScreen; /* ─── Styles factory ─── */ const makeStyles = (colors: any, spacing: any, shadows: any) => StyleSheet.create({ root: { flex: 1, backgroundColor: colors.background, }, /* Info band */ infoBand: { backgroundColor: colors.surface, paddingHorizontal: spacing.m, paddingVertical: spacing.m, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.border, gap: spacing.s, ...shadows.light, }, deptRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.s, }, deptIcon: { width: 30, height: 30, borderRadius: 9, alignItems: 'center', justifyContent: 'center', }, deptName: { flex: 1, } as any, /* Timer */ timerCard: { borderRadius: 16, borderWidth: 1.2, paddingVertical: spacing.m, paddingHorizontal: spacing.m, alignItems: 'center', gap: 4, }, timerLabelRow: { flexDirection: 'row', alignItems: 'center', gap: 4, }, timerLabel: { letterSpacing: 1.2, } as any, /* Chips */ chipRow: { flexDirection: 'row', gap: spacing.s, }, chip: { flexDirection: 'row', alignItems: 'center', gap: 4, paddingHorizontal: spacing.s + 4, paddingVertical: 5, borderRadius: 20, }, chipText: { letterSpacing: 0.3, } as any, liveDot: { width: 6, height: 6, borderRadius: 3, }, listContent: { padding: spacing.m, paddingBottom: 120, }, overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: colors.overlay, justifyContent: 'center', alignItems: 'center', zIndex: 99, }, successLottie: { width: 200, height: 200 }, });