second commit

This commit is contained in:
rohit-kumar-fullstack
2026-01-07 10:19:34 +05:30
parent 59fb7e0df4
commit 743d571519
29 changed files with 16782 additions and 619 deletions
@@ -0,0 +1,218 @@
import React, { memo, useCallback, useRef } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Platform } from 'react-native';
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import colors from '../../../Constant/Color';
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
import MainStyle from '../../../Styles/MainStyle';
const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction }: any) => {
const [showDetail, setShowDetail] = React.useState(false);
const isMin = bidValue <= item.bidAmount;
// 🔒 Persist expand state safely
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 arrowStyle = useAnimatedStyle(() => ({
transform: [
{
rotate: `${interpolate(animation.value, [0, 1], [0, 180])}deg`,
},
],
}));
return (
<View>
{
!showDetail ? <Animated.View
layout={Layout.springify()}
entering={FadeInUp.delay(100)}
style={[styles.card, styles.leadingCard, { borderColor: item.me ? '#22C55E' : 'red', backgroundColor: item.me ? "#F0FDF4" : '#fdf0f0ff' }]}
>
{/* HEADER */}
<TouchableOpacity
activeOpacity={0.7}
onPress={() => { setShowDetail(true) }}
style={styles.cardHeader}
>
<View style={{ flex: 1 }}>
<Text style={styles.productName}>{item.productName}</Text>
{/* auctionStartValueFigure */}
<Text style={styles.itemIdText}>
Initial Amt : {item?.auctionStartValueFigure}
</Text>
</View>
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
<View style={[MainStyle.flexBetween, { gap: 10 }]}>
<View style={[styles.badge, item.me ? styles.meBadge : styles.liveBadge]}>
<Text style={[styles.badgeText, item.me ? styles.meText : styles.liveText]}>
{item.me ? 'LEADING' : 'LIVE'}
</Text>
</View>
<Animated.Text style={[styles.arrow, arrowStyle]}>
▼
</Animated.Text>
</View>
<Text style={styles.itemIdText}>
{SelectedAuction?.auctionNumber}
</Text>
</TouchableOpacity>
</TouchableOpacity>
{/* PRICE INFO */}
<View style={styles.priceRow}>
<View>
<Text style={styles.label}>Current Bid</Text>
<Text style={styles.mainPrice}>₹{item?.bidAmount}</Text>
</View>
<View style={{ alignItems: 'flex-end' }}>
<Text style={styles.label}>Increment</Text>
<Text style={styles.incrementText}>
+₹{item.bidVariationValue}
</Text>
</View>
</View>
{/* COLLAPSIBLE BODY */}
<Animated.View style={[styles.collapsibleContainer, bodyStyle]}>
<View style={styles.divider} />
<Text style={styles.bidLabel}>Set Your Bid</Text>
<View style={styles.stepperContainer}>
<TouchableOpacity
onPress={onDecrease}
disabled={isMin}
style={[styles.stepBtn, isMin && styles.disabledBtn]}
>
<Text style={styles.stepText}>−</Text>
</TouchableOpacity>
<View style={styles.inputBox}>
<Text style={styles.currency}>₹</Text>
<Text style={styles.bidValueText}>{bidValue}</Text>
</View>
<TouchableOpacity onPress={onIncrease} style={styles.stepBtn}>
<Text style={styles.stepText}>+</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
style={styles.submitBtn}
onPress={onPlaceBid}
// activeOpacity={0.85}
>
<Text style={styles.submitBtnText}>Confirm Bid</Text>
</TouchableOpacity>
</Animated.View>
</Animated.View>
:
<AuctionItemDetail data={item} onClose={() => { setShowDetail(false) }} visible={showDetail} />
}
</View>
)
};
export default memo(BiddingCard);
const styles = StyleSheet.create({
card: {
backgroundColor: '#FFF',
borderRadius: 24,
padding: 20,
marginBottom: 16,
borderWidth: 1,
borderColor: '#E2E8F0',
overflow: 'hidden',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 10,
},
android: { elevation: 2 },
}),
},
leadingCard: {
borderColor: '#22C55E',
borderWidth: 2,
backgroundColor: '#F0FDF4',
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
},
headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
liveBadge: { backgroundColor: '#FEE2E2' },
meBadge: { backgroundColor: '#BBF7D0' },
badgeText: { fontSize: 10, fontWeight: '900' },
liveText: { color: '#EF4444' },
meText: { color: '#15803D' },
arrow: { fontSize: 10, color: '#94A3B8' },
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
label: {
fontSize: 11,
color: '#94A3B8',
textTransform: 'uppercase',
fontWeight: '600',
},
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' },
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: {
width: 45,
height: 45,
backgroundColor: colors.primary,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
},
disabledBtn: { backgroundColor: '#CBD5E1' },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
inputBox: {
flex: 1,
height: 45,
backgroundColor: '#F1F5F9',
borderRadius: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
submitBtn: {
height: 50,
backgroundColor: colors.primary,
borderRadius: 15,
justifyContent: 'center',
alignItems: 'center',
},
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' },
});
@@ -0,0 +1,370 @@
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 from '../../../Constant/Color';
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
import MainStyle from '../../../Styles/MainStyle';
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 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,
{
borderColor: item.me ? '#22C55E' : '#EF4444',
backgroundColor: item.me ? '#F0FDF4' : '#FDF0F0',
},
]}
>
{/* HEADER */}
<TouchableOpacity
activeOpacity={0.7}
onPress={() => setShowDetail(true)}
style={styles.cardHeader}
>
<View style={{ flex: 1 }}>
<Text style={styles.productName}>{item.productName}</Text>
<Text style={styles.itemIdText}>
Initial Amt : ₹{item.auctionStartValue}
</Text>
</View>
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
<View style={[MainStyle.flexBetween, { gap: 10 }]}>
<View
style={[
styles.badge,
item.me ? styles.meBadge : styles.liveBadge,
]}
>
<Text
style={[
styles.badgeText,
item.me ? styles.meText : styles.liveText,
]}
>
{item.me ? 'LEADING' : 'LIVE'}
</Text>
</View>
<Animated.Text style={[styles.arrow, arrowStyle]}>
▼
</Animated.Text>
</View>
<Text style={styles.itemIdText}>
{SelectedAuction?.auctionNumber}
</Text>
</TouchableOpacity>
</TouchableOpacity>
{/* PRICE INFO */}
<View style={styles.priceRow}>
<View>
<Text style={styles.label}>Current Bid</Text>
<Text style={styles.mainPrice}>₹{item.bidAmount || item.auctionStartValue}</Text>
</View>
<View style={{ alignItems: 'flex-end' }}>
<Text style={styles.label}>Decrement</Text>
<Text style={styles.incrementText}>
-₹{item.emdAmount}
</Text>
</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} />
<Text style={styles.bidLabel}>Set Your Bid</Text>
<View style={styles.stepperContainer}>
{/* DECREASE */}
{!allBoolean.oneTimeBid && <TouchableOpacity
onPress={onDecrease}
disabled={!canDecrease}
style={[
styles.stepBtn,
!canDecrease && styles.disabledBtn,
]}
>
<Text style={styles.stepText}>−</Text>
</TouchableOpacity>}
{/* VALUE */}
{!allBoolean.oneTimeBid && <View style={styles.inputBox}>
<Text style={styles.currency}>₹</Text>
<Text style={styles.bidValueText}>{bidValue}</Text>
</View>}
{
(allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}>
<Text style={styles.currency2}>₹</Text>
<TextInput
value={value}
onChangeText={handleChange}
keyboardType="decimal-pad"
placeholder="0.00"
style={styles.input}
placeholderTextColor="#94A3B8"
/>
</View>
}
{/* INCREASE */}
{!allBoolean.oneTimeBid && <TouchableOpacity
onPress={onIncrease}
disabled={!canIncrease}
style={[
styles.stepBtn,
!canIncrease && styles.disabledBtn,
]}
>
<Text style={styles.stepText}>+</Text>
</TouchableOpacity>}
</View>
<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 && 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 styles = StyleSheet.create({
inputBox2: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F1F5F9',
borderRadius: 12,
height: 45,
paddingHorizontal: 12,
width: '100%'
},
currency2: {
fontSize: 16,
fontWeight: 'bold',
color: '#64748B',
marginRight: 4,
},
input: {
flex: 1,
fontSize: 18,
fontWeight: '700',
color: '#0F172A',
width: '100%'
},
card: {
backgroundColor: '#FFF',
borderRadius: 24,
padding: 20,
marginBottom: 16,
borderWidth: 1,
borderColor: '#E2E8F0',
overflow: 'hidden',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 10,
},
android: { elevation: 2 },
}),
},
leadingCard: {
borderColor: '#22C55E',
borderWidth: 2,
backgroundColor: '#F0FDF4',
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
},
headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
liveBadge: { backgroundColor: '#FEE2E2' },
meBadge: { backgroundColor: '#BBF7D0' },
badgeText: { fontSize: 10, fontWeight: '900' },
liveText: { color: '#EF4444' },
meText: { color: '#15803D' },
arrow: { fontSize: 10, color: '#94A3B8' },
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
label: {
fontSize: 11,
color: '#94A3B8',
textTransform: 'uppercase',
fontWeight: '600',
},
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' },
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: {
width: 45,
height: 45,
backgroundColor: colors.primary,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
},
disabledBtn: { backgroundColor: '#CBD5E1' },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
inputBox: {
flex: 1,
height: 45,
backgroundColor: '#F1F5F9',
borderRadius: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
submitBtn: {
height: 50,
backgroundColor: colors.primary,
borderRadius: 15,
justifyContent: 'center',
alignItems: 'center',
},
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' },
});
@@ -0,0 +1,416 @@
import React, { useState } from 'react'
import { Modal, View, Text, StyleSheet, TextInput, TouchableOpacity, ScrollView } from 'react-native'
import { Formik } from 'formik'
import * as Yup from 'yup'
import { pick } from '@react-native-documents/picker'
import { useSelector } from 'react-redux'
import { apiCall } from '../../../Axios/Axios'
import { TECHNICAL_DOCUMENT_SUBMIT, TECHNICAL_VALUE_SUBMIT } from '../../../Services/BBPS/ApiUrls'
import { Props } from '../Type/BidType'
import colors from '../../../Constant/Color'
import { Loader } from '../../../Component/Index'
import { useNavigation } from '@react-navigation/native'
import NavigationString from '../../../Constant/NavigationString'
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 [loading, setLoading] = useState(false)
const initialValues = template.labels.reduce((acc: any, label: any) => {
acc[label.labelName] = ''
return acc
}, {})
const validationSchema = Yup.object(
template.labels.reduce((acc: any, label: any) => {
if (label.mandatory) {
acc[label.labelName] =
label.labelType === '3'
? Yup.mixed().required(`${label.labelName} is required`)
: Yup.string().required(`${label.labelName} is required`)
}
return acc
}, {})
)
const pickFile = async (fieldName: string, setFieldValue: any) => {
try {
const result = await pick({
type: ['application/pdf'],
allowMultiSelection: false,
})
const file = result[0]
setFieldValue(fieldName, {
name: file.name ?? 'document.pdf',
uri: file.uri,
type: 'application/pdf',
size: file.size,
})
} catch (e) {
console.log(e)
}
}
const renderInput = (
label: any,
value: any,
setFieldValue: any,
error?: string,
touched?: boolean
) => {
const showError = touched && error
if (label.labelType === '3') {
return (
<View key={label.labelName} style={styles.field}>
<Text style={styles.label}>
{label.labelName}
{label.mandatory && <Text style={styles.required}> *</Text>}
</Text>
<TouchableOpacity
style={[
styles.filePicker,
showError && styles.errorBorder,
]}
onPress={() =>
pickFile(label.labelName, setFieldValue)
}
>
<Text
style={[
styles.fileText,
value?.name && styles.fileSelected,
]}
>
{value?.name || 'Upload PDF'}
</Text>
</TouchableOpacity>
{showError && (
<Text style={styles.errorText}>{error}</Text>
)}
</View>
)
}
return (
<View key={label.labelName} style={styles.field}>
<Text style={styles.label}>
{label.labelName}
{label.mandatory && <Text style={styles.required}> *</Text>}
</Text>
<TextInput
style={[
styles.input,
showError && styles.errorBorder,
]}
placeholder={`Enter ${label.labelName}`}
placeholderTextColor="#9CA3AF"
value={value}
onChangeText={(text) =>
setFieldValue(label.labelName, text)
}
keyboardType={
label.labelType === '1' || label.labelType === '4'
? 'numeric'
: 'default'
}
/>
{showError && (
<Text style={styles.errorText}>{error}</Text>
)}
</View>
)
}
const onClose = () => {
setAllBoolean((prev: any) => ({
...prev,
technicalDocModalVisible: false,
}))
}
const onSubmit = async (payload: any) => {
try {
const jsonBody = {
auctiontId: SelectedAuction.auctionId,
technicalDocumentTemplateId: payload.templateNameId,
labelValues: [] as any[],
}
const fileFormData = new FormData()
let hasFile = false
Object.entries(payload.values).forEach(
([labelName, labelValue]: any) => {
if (typeof labelValue === 'object' && labelValue?.uri) {
hasFile = true
fileFormData.append(labelName, {
uri: labelValue.uri,
name: labelValue.name,
type: labelValue.type,
} as any)
} else {
jsonBody.labelValues.push({
labelName,
labelValue: String(labelValue),
})
}
}
)
const rs1: any = await apiCall('post', TECHNICAL_VALUE_SUBMIT, jsonBody)
if (hasFile) {
fileFormData.append(
'auctiontId',
String(SelectedAuction.auctionId)
)
fileFormData.append(
'technicalDocumentTemplateId',
String(payload.templateNameId)
)
const res2: any = await apiCall(
'post',
TECHNICAL_DOCUMENT_SUBMIT,
fileFormData,
{},
'multipart/form-data'
)
console.log(res2, '-------------res2');
if (rs1.statusCode == 200 && res2.statusCode == 200) {
Navigation.navigate(NavigationString.StartBidding)
setAllBoolean((prev: any) => ({ ...prev, technicalDocModalVisible: false }))
}
} else {
if (rs1.statusCode == 200) {
Navigation.navigate(NavigationString.StartBidding)
setAllBoolean((prev: any) => ({ ...prev, technicalDocModalVisible: false }))
}
}
} catch (e) {
console.log(e)
} finally {
setLoading(false)
}
}
return (
<Modal visible={visible} animationType="fade" transparent>
<View style={styles.screen}>
<View style={styles.header}>
<View style={styles.headerLeft}>
<Text style={styles.headerTitle}>Technical Documents</Text>
<Text style={styles.headerSubtitle}>
Fill the form, Click save when you're done.
</Text>
</View>
<TouchableOpacity onPress={onClose} style={styles.closeBtn}>
<Text style={styles.closeText}>✕</Text>
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={styles.content}>
<View style={styles.card}>
<Text style={styles.templateHeading}>Template</Text>
<View style={styles.templateBox}>
<Text style={styles.templateName}>
{template.templateName}
</Text>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={(values) => {
setLoading(true)
onSubmit({
templateNameId: template.templateNameId,
auctionIds: template.auctionIds,
values,
})
}}
>
{({ handleSubmit, values, errors, touched, setFieldValue }) => (
<>
{template.labels.map((label: any) =>
renderInput(
label,
values[label.labelName],
setFieldValue,
// errors[label.labelName],
// touched[label.labelName]
)
)}
<View style={styles.submitRow}>
<TouchableOpacity
style={styles.submitBtn}
onPress={() => handleSubmit()}
disabled={loading}
>
{loading ? (
<Loader
size="small"
color={colors.white}
/>
) : (
<Text style={styles.submitText}>
Submit
</Text>
)}
</TouchableOpacity>
</View>
</>
)}
</Formik>
</View>
</View>
</ScrollView>
</View>
</Modal>
)
}
export default TemplateFormModal
const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: '#F9FAFB',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 14,
backgroundColor: '#FFFFFF',
borderBottomWidth: 1,
borderColor: '#E5E7EB',
},
headerLeft: {
flex: 1,
},
headerTitle: {
fontSize: 16,
fontWeight: '700',
color: '#111827',
},
headerSubtitle: {
fontSize: 12,
color: '#6B7280',
marginTop: 2,
},
closeBtn: {
padding: 6,
},
closeText: {
fontSize: 18,
color: '#6B7280',
},
content: {
paddingHorizontal: 16,
},
card: {
backgroundColor: '#FFFFFF',
borderColor: '#E5E7EB',
paddingVertical: 20
},
templateHeading: {
fontSize: 18,
fontWeight: '700',
textAlign: 'center',
marginBottom: 16,
color: '#111827',
},
templateBox: {
borderWidth: 1,
borderColor: '#E5E7EB',
borderRadius: 10,
padding: 16,
},
templateName: {
fontSize: 14,
fontWeight: '700',
color: '#111827',
marginBottom: 12,
},
field: {
marginBottom: 14,
},
label: {
fontSize: 13,
fontWeight: '600',
color: '#374151',
marginBottom: 6,
},
required: {
color: '#EF4444',
},
input: {
backgroundColor: '#FFFFFF',
borderWidth: 1,
borderColor: '#E5E7EB',
borderRadius: 8,
padding: 12,
fontSize: 14,
color: '#111827',
},
filePicker: {
borderWidth: 1,
borderColor: '#E5E7EB',
borderRadius: 8,
padding: 12,
},
fileText: {
color: '#6B7280',
fontSize: 14,
},
fileSelected: {
color: '#111827',
fontWeight: '600',
},
errorText: {
color: '#EF4444',
fontSize: 12,
marginTop: 4,
},
errorBorder: {
borderColor: '#EF4444',
},
submitRow: {
marginTop: 20,
alignItems: 'flex-end',
height:50
},
submitBtn: {
backgroundColor: colors.primary,
paddingVertical: 10,
paddingHorizontal: 22,
borderRadius: 8,
minWidth: 120,
alignItems: 'center',
},
submitText: {
color: '#FFFFFF',
fontWeight: '700',
fontSize: 14,
},
})
+201 -46
View File
@@ -1,37 +1,30 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, TouchableOpacity, StyleSheet, TextInput, Alert } from 'react-native';
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, Loader, Skelton } from '../../Component/Index';
import colors from '../../Constant/Color';
import EmdComplete from './Component/EmdComplete';
import NavigationString from '../../Constant/NavigationString';
import { useNavigation } from '@react-navigation/native';
import { useSelector } from 'react-redux';
import { useAuctionItem, useEmdCheck } from '../../Services/BBPS/Hooks';
import { apiCall } from '../../Axios/Axios';
import { AUCTION_ITEM_SUBMIT, CHECK_TECHNICAL_DOCUMENTAION, GET_TECHNICAL_DOCUMENTAION } from '../../Services/BBPS/ApiUrls';
import TemplateFormModal from './Component/TemplateFormModal';
import { SelectedItem } from './Type/BidType';
const DUMMY_AUCTION_ITEMS = [
{ id: 1, productName: 'Iron Scrap Lot A', emdAmount: 50000, itemDocumentName: null },
{ id: 2, productName: 'Steel Rods Lot B', emdAmount: 75000, itemDocumentName: 'old_emd.pdf' },
{ id: 3, productName: 'Copper Wire Lot C', emdAmount: 60000, itemDocumentName: null },
];
type SelectedItem = {
auctionItemId: number;
productImageName: string;
file: {
name: string;
uri: string;
type: string;
} | null;
};
const Emd = () => {
const Navigation: any = useNavigation()
const SelectedAuction = useSelector((state: any) => state.auction.auction)
const [selectedItems, setSelectedItems] = useState<SelectedItem[]>([]);
const [acceptedPolicy, setAcceptedPolicy] = useState(false);
const [isComplete, setIsComplete] = useState(false);
const [allBoolean, setAllBoolean] = useState({ isSubmitEmdLoading: false, isEmdComplete: false, auctionItems: [], technicalDocumentStatus: false, selectTemplate: {}, technicalDocModalVisible: false })
const { mutate } = useEmdCheck()
const { mutate: AuctionItemMutate, isPending } = useAuctionItem()
/* ---------------- Helpers ---------------- */
const isSelected = (id: number) => selectedItems.some(i => i.auctionItemId === id);
const toggleSelect = (item: any, checked: boolean) => {
@@ -70,27 +63,6 @@ const Emd = () => {
};
const handleSubmit = () => {
if (!acceptedPolicy) {
Alert.alert('Policy Required', 'Please accept terms');
return;
}
if (selectedItems.length === 0) {
Alert.alert('Select Item', 'Please select at least one item');
return;
}
for (const item of selectedItems) {
if (!item.productImageName || !item.file) {
Alert.alert('Incomplete', 'Upload documents for all selected items');
return;
}
}
console.log('✅ SUBMITTED:', selectedItems);
setIsComplete(true);
};
/* ---------------- Render Item ---------------- */
const renderItem = ({ item }: any) => {
@@ -141,6 +113,7 @@ const Emd = () => {
updateSelectedItem(item.id, { productImageName: text })
}
style={styles.input}
placeholderTextColor={colors.lightText}
/>
<TouchableOpacity
@@ -163,19 +136,197 @@ const Emd = () => {
);
};
// Api calls
const getAuctionItem = async () => {
try {
setAllBoolean((prev: any) => ({ ...prev, isLoading: true }))
if (SelectedAuction.auctionPattern === 'Forward') {
const payload = { auctionNumber: SelectedAuction.auctionNumber, status: 'Forward' }
AuctionItemMutate(payload, {
onSuccess: (res) => {
if (res.statusCode == 200) {
setAllBoolean((prev: any) => ({ ...prev, auctionItems: Array.isArray(res?.data) ? res.data : [res.data] }))
}
},
onError: (err) => {
}
})
} else if (SelectedAuction.auctionPattern === 'Reverse') {
const payload = { auctionNumber: SelectedAuction.auctionNumber, status: 'Reverse' }
AuctionItemMutate(payload, {
onSuccess: (res) => {
if (res.statusCode == 200) {
setAllBoolean((prev: any) => ({ ...prev, auctionItems: Array.isArray(res?.data) ? res.data : [res.data] }))
}
},
onError: (err) => {
}
})
}
} catch (error) {
console.log('Auction fetch error', error)
} finally {
setAllBoolean((prev: any) => ({ ...prev, isLoading: false }))
}
}
const getTemplateDocumentation = async () => {
try {
if (SelectedAuction.technicalDocReq) {
const res = await apiCall<any>('get', CHECK_TECHNICAL_DOCUMENTAION, {}, { auctionId: SelectedAuction.auctionId })
if (res.data) {
setAllBoolean((prev: any) => ({ ...prev, technicalDocumentStatus: true }))
} else {
const res = await apiCall<any>('get', GET_TECHNICAL_DOCUMENTAION,)
const auctionIdToFind = String(SelectedAuction.auctionId);
const matchedObject = res.data.find((item: any) =>
item.auctionIds
.split(',')
.map((id: any) => id.trim())
.includes(auctionIdToFind)
);
setAllBoolean((prev: any) => ({ ...prev, selectTemplate: matchedObject, technicalDocModalVisible: true }))
}
}
} catch (error) {
console.log('Auction fetch error', error)
} finally {
setAllBoolean((prev: any) => ({ ...prev, isLoading: false }))
}
}
const AlreadyEmdCheck = async () => {
try {
mutate({ auctionNumber: SelectedAuction.auctionNumber }, {
onSuccess: (res => {
console.log(SelectedAuction.auctionNumber, res , SelectedAuction,'iiiiiiiiiiiiiiiiiiiiiiiii');
if (res.statusCode == 200) {
if (res.message == 'No EMD Documents') {
setAllBoolean((prev: any) => ({ ...prev, isEmdComplete: false }))
getAuctionItem()
} else {
if (!SelectedAuction.technicalDocReq) {
setAllBoolean((prev: any) => ({ ...prev, isEmdComplete: true, technicalDocumentStatus: true }))
} else {
setAllBoolean((prev: any) => ({ ...prev, isEmdComplete: true }))
getTemplateDocumentation()
}
}
}
}),
onError: (err) => {
}
})
} catch (error) {
console.log('EMD Check Error:', error)
} finally {
setAllBoolean((prev: any) => ({ ...prev, isLoading: false }))
}
}
const handleSubmit = async () => {
setAllBoolean((prev: any) => ({ ...prev, isSubmitEmdLoading: true }))
if (!acceptedPolicy) {
Alert.alert('Policy Required', 'Please accept terms');
return;
}
if (selectedItems.length === 0) {
Alert.alert('Select Item', 'Please select at least one item');
return;
}
for (const item of selectedItems) {
if (!item.productImageName || !item.file) {
Alert.alert('Incomplete', 'Upload documents for all selected items');
return;
}
}
try {
const formData = new FormData()
formData.append('auctionId', String(SelectedAuction.auctionId))
selectedItems.forEach((item: any) => {
formData.append(
'auctionItemId',
String(item.auctionItemId)
)
formData.append(
'productImageName',
item.productImageName
)
formData.append('files', {
uri: item.file.uri,
name: item.file.name,
type: item.file.type || 'application/pdf',
} as any)
})
const res = await apiCall<any>('post', AUCTION_ITEM_SUBMIT, formData, {}, 'multipart/form-data')
if (res.statusCode == 200) {
if (SelectedAuction.technicalDocReq) {
getTemplateDocumentation()
} else {
if (SelectedAuction.auctionPattern === 'Forward') {
Navigation.navigate(NavigationString.StartBidding)
} else {
Navigation.navigate(NavigationString.StartReverseBidding)
}
}
}
} catch (error) {
console.log('Submit error', error)
Alert.alert('Error', 'Failed to submit EMD')
} finally {
setAllBoolean((prev: any) => ({ ...prev, isSubmitEmdLoading: true }))
}
};
const EmdContinueHandle = async () => {
if (allBoolean.technicalDocumentStatus) {
if (SelectedAuction.auctionPattern === 'Forward') {
Navigation.navigate(NavigationString.StartBidding)
} else {
Navigation.navigate(NavigationString.StartReverseBidding)
}
} else {
getTemplateDocumentation()
}
}
useEffect(() => {
AlreadyEmdCheck()
}, [])
return (
<View style={{ flex: 1, backgroundColor: colors.white }}>
<InsideHeader title="EMD Submission" showArrow />
<View style={styles.container}>
{SelectedAuction.technicalDocReq && allBoolean.technicalDocModalVisible &&
<TemplateFormModal visible={allBoolean.technicalDocModalVisible} allBoolean={allBoolean} setAllBoolean={setAllBoolean} template={allBoolean.selectTemplate} />
}
{false ? (
{isPending ? (
<Skelton />
) : true ? (
<EmdComplete onNext={() => { Navigation.navigate(NavigationString.StartBidding) }} />
) : allBoolean.isEmdComplete ? (
<EmdComplete onNext={EmdContinueHandle} />
) : <>
<FlatList
data={DUMMY_AUCTION_ITEMS}
keyExtractor={item => item.id.toString()}
data={allBoolean.auctionItems}
keyExtractor={(item: any) => item.auctionId}
renderItem={renderItem}
contentContainerStyle={{ paddingBottom: 170 }}
/>
@@ -200,7 +351,10 @@ const Emd = () => {
onPress={handleSubmit}
disabled={!acceptedPolicy}
>
<Text style={styles.submitText}>Submit EMD</Text>
{
allBoolean.isSubmitEmdLoading ? <Loader size='small' color={colors.white} /> : <Text style={styles.submitText}>Submit EMD</Text>
}
</TouchableOpacity>
</View>
</>
@@ -277,6 +431,7 @@ const styles = StyleSheet.create({
padding: 12,
backgroundColor: '#FFFFFF',
fontSize: 14,
color: colors.black
},
uploadBtn: {
+243 -152
View File
@@ -1,154 +1,136 @@
import React, { useEffect, useState, useCallback } from 'react';
import {
View,
Text,
FlatList,
TextInput,
TouchableOpacity,
StyleSheet,
Platform,
Alert,
} from 'react-native';
import React, { useCallback, useEffect, useState } from 'react';
import { View, FlatList, StyleSheet, Platform, Alert, BackHandler, Text } from 'react-native';
import LottieView from 'lottie-react-native';
import Animated, {
FadeInUp,
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
Layout,
interpolate,
} from 'react-native-reanimated';
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 { FORWARD_AUCTION_ITEM, REVERSE_AUCTION_ITEM, SELECTED_AUCTION_ITEM } from '../../Services/BBPS/ApiUrls';
import BiddingCard from './Component/BiddingCard';
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
import CountdownTimer from '../../Component/Counter/CountdownTimer';
import FontsFamily from '../../Constant/FontsFamily';
/* ---------------- Types ---------------- */
interface AuctionItem {
id: number;
productName: string;
auctionStartValueFigure: number;
bidVariationValue: number;
bidAmount: number;
bidAvilable: number;
me: boolean;
}
const StartBidding = () => {
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, bidPayload: {} })
const { subscribe, createRoom, placeBid } = useWebSocketService(
Variables.webSocketUrl,
() => console.log('Connected!'),
(error) => console.log('WebSocket Error:', error),
loginUser.token,
);
/* ---------------- Dummy Data ---------------- */
const DUMMY_AUCTION = { auctionNumber: 'AUC-2025-001' };
const DUMMY_ITEMS: AuctionItem[] = [
{ id: 1, productName: 'Iron Scrap Lot', auctionStartValueFigure: 20000, bidVariationValue: 500, bidAmount: 20000, bidAvilable: 20000, me: false },
{ id: 2, productName: 'Steel Rod Bundle', auctionStartValueFigure: 35000, bidVariationValue: 1000, bidAmount: 35000, bidAvilable: 35000, me: true },
{ id: 3, productName: 'Copper Wire Roll', auctionStartValueFigure: 15000, bidVariationValue: 250, bidAmount: 15000, bidAvilable: 0, me: false },
];
const getAllAcutionItems = async () => {
try {
const payload = { auctionNumber: SelectedAuction.auctionNumber };
let auctionItem: any[] = [];
/* ---------------- Bidding Card ---------------- */
const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid }: any) => {
const [expanded, setExpanded] = useState(false);
const isMin = bidValue <= item.bidAmount;
if (SelectedAuction.auctionPattern === 'Forward') {
const res = await apiCall<any>('get', FORWARD_AUCTION_ITEM, {}, payload);
if (res?.statusCode === 200) auctionItem = res.data || [];
} else {
const res = await apiCall<any>('get', REVERSE_AUCTION_ITEM, {}, payload);
if (res?.statusCode === 200) auctionItem = res.data || [];
}
const animation = useSharedValue(0);
const selectedRes = await apiCall<any>('get', SELECTED_AUCTION_ITEM, {}, payload);
console.log(selectedRes,'jhhhhhhhhhh');
const toggleExpand = () => {
setExpanded(!expanded);
animation.value = withSpring(expanded ? 0 : 1, { damping: 15 });
const uniqueArray: number[] = Array.from(
new Set(selectedRes?.data || [])
);
const filteredItems = auctionItem.filter(item =>
uniqueArray.includes(item.id)
);
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,
});
});
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.auctionStartValueFigure,
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);
} catch (error) {
console.error('Error fetching auction items:', error);
}
};
const bodyStyle = useAnimatedStyle(() => {
return {
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]),
};
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, async (message) => {
// console.log('RAW MESSAGE:', message[message.length - 1]);
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);
}
});
const arrowStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${interpolate(animation.value, [0, 1], [0, 180])}deg` }],
}));
useEffect(() => {
getAllAcutionItems();
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, (data) => { });
}, []);
return (
<Animated.View
layout={Layout.springify()}
entering={FadeInUp.delay(item.id * 100)}
style={[styles.card, item.me && styles.leadingCard]}
>
{/* Header - Always Visible */}
<TouchableOpacity
activeOpacity={0.7}
onPress={toggleExpand}
style={styles.cardHeader}
>
<View style={{ flex: 1 }}>
<Text style={styles.productName}>{item.productName}</Text>
<Text style={styles.itemIdText}>{DUMMY_AUCTION.auctionNumber}</Text>
</View>
<View style={styles.headerRight}>
<View style={[styles.badge, item.me ? styles.meBadge : styles.liveBadge]}>
<Text style={[styles.badgeText, item.me ? styles.meText : styles.liveText]}>
{item.me ? 'LEADING' : 'LIVE'}
</Text>
</View>
<Animated.Text style={[styles.arrow, arrowStyle]}>▼</Animated.Text>
</View>
</TouchableOpacity>
{/* Main Price Info - Always Visible */}
<View style={styles.priceRow}>
<View>
<Text style={styles.label}>Current Bid</Text>
<Text style={styles.mainPrice}>₹{item.bidAmount.toLocaleString()}</Text>
</View>
<View style={{ alignItems: 'flex-end' }}>
<Text style={styles.label}>Increment</Text>
<Text style={styles.incrementText}>+₹{item.bidVariationValue}</Text>
</View>
</View>
{/* Collapsible Section */}
<Animated.View style={[styles.collapsibleContainer, bodyStyle]}>
<View style={styles.divider} />
<Text style={styles.bidLabel}>Set Your Bid</Text>
<View style={styles.stepperContainer}>
<TouchableOpacity
onPress={onDecrease}
disabled={isMin}
style={[styles.stepBtn, isMin && styles.disabledBtn]}
>
<Text style={styles.stepText}>−</Text>
</TouchableOpacity>
<View style={styles.inputBox}>
<Text style={styles.currency}>₹</Text>
<Text style={styles.bidValueText}>{bidValue.toLocaleString()}</Text>
</View>
<TouchableOpacity onPress={onIncrease} style={styles.stepBtn}>
<Text style={styles.stepText}>+</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
style={styles.submitBtn}
onPress={onPlaceBid}
activeOpacity={0.8}
>
<Text style={styles.submitBtnText}>Confirm Bid</Text>
</TouchableOpacity>
</Animated.View>
</Animated.View>
);
};
/* ---------------- Main Screen ---------------- */
const StartBidding = () => {
const [items, setItems] = useState<AuctionItem[]>(DUMMY_ITEMS);
const [bidValues, setBidValues] = useState<Record<number, number>>({});
const [showSuccess, setShowSuccess] = useState(false);
useEffect(() => {
const initial: Record<number, number> = {};
items.forEach(item => {
items.forEach((item: any) => {
initial[item.id] = item.bidAmount + item.bidVariationValue;
});
setBidValues(initial);
@@ -158,38 +140,133 @@ const StartBidding = () => {
setBidValues(prev => ({ ...prev, [id]: prev[id] + val }));
};
const handlePlaceBid = (item: AuctionItem) => {
Alert.alert('Confirm', `Place bid for ₹${bidValues[item.id]}?`, [
{ text: 'No' },
{
text: 'Yes, Bid',
onPress: () => {
setItems(prev => prev.map(i => i.id === item.id ? { ...i, bidAmount: bidValues[item.id], me: true } : i));
setShowSuccess(true);
setTimeout(() => setShowSuccess(false), 2000);
}
}
]);
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, }));
Alert.alert(
'Confirm Bid',
`Are you sure you want to place a bid of ₹${payload.bidAmount}?`,
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Submit',
onPress: () => {
placeBidSubmit(payload);
},
},
],
{ cancelable: true }
);
};
const placeBidSubmit = async (data: any) => {
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
);
// setItems(updatedBidItem);
// setBidValues((prev: any) => ({
// ...prev,
// [result.data.auctionItemId]: {
// ...prev[result.data.auctionItemId],
// bidAmount: result.data.bidAmount,
// },
// }));
getAllAcutionItems()
}
} catch (error) {
console.error('Error placing bid:', error);
}
};
useFocusEffect(
useCallback(() => {
const onBackPress = () => {
Alert.alert(
'Confirm',
'Do you want to go back to Dashboard?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Yes',
onPress: () => {
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
},
},
],
{ cancelable: 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}>
<Text style={styles.title}>
{SelectedAuction.deptName}
</Text>
<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 }) => (
renderItem={({ item, index }) => (
<BiddingCard
item={item}
item={{ ...item, index }}
bidValue={bidValues[item.id] || 0}
onIncrease={() => handleUpdate(item.id, item.bidVariationValue)}
onDecrease={() => handleUpdate(item.id, -item.bidVariationValue)}
onPlaceBid={() => handlePlaceBid(item)}
onPlaceBid={() => { submitBid(item) }}
SelectedAuction={SelectedAuction}
/>
)}
contentContainerStyle={{ padding: 16 }}
/>
{showSuccess && (
{allBoolean.showSuccess && (
<View style={styles.overlay}>
<LottieView
source={require('../../lottie/Success.json')}
@@ -205,9 +282,23 @@ const StartBidding = () => {
export default StartBidding;
/* ---------------- Modern Styles ---------------- */
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.white },
container2: {
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 8,
},
title: {
fontSize: 18,
fontFamily: FontsFamily.poppinsSemiBold,
color: colors.black,
marginBottom: 6,
// textAlign:'center'
},
timerWrapper: {
alignItems: 'center',
},
card: {
backgroundColor: '#FFF',
borderRadius: 24,
+401
View File
@@ -0,0 +1,401 @@
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/BBPS/ApiUrls';
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
import CountdownTimer from '../../Component/Counter/CountdownTimer';
import FontsFamily from '../../Constant/FontsFamily';
import ReverseBiddingCard from './Component/ReverseBiddingCard';
const StartReverseBidding = () => {
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, bidPayload: {}, 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, }));
Alert.alert(
'Confirm Bid',
`Are you sure you want to place a bid of ₹${payload.bidAmount}?`,
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Submit',
onPress: () => {
placeBidSubmit(payload);
},
},
],
{ cancelable: true }
);
};
const placeBidSubmit = async (data: any) => {
try {
const result: any = await placeBid(data);
console.log(result, 'kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk');
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 = () => {
Alert.alert(
'Confirm',
'Do you want to go back to Dashboard?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Yes',
onPress: () => {
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
},
},
],
{ cancelable: 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}>
<Text style={styles.title}>
{SelectedAuction.deptName}
</Text>
<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>
)}
</View>
);
};
export default StartReverseBidding;
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.white },
container2: {
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 8,
},
title: {
fontSize: 18,
fontFamily: FontsFamily.poppinsSemiBold,
color: colors.black,
marginBottom: 6,
// textAlign:'center'
},
timerWrapper: {
alignItems: 'center',
},
card: {
backgroundColor: '#FFF',
borderRadius: 24,
padding: 20,
marginBottom: 16,
borderWidth: 1,
borderColor: '#E2E8F0',
overflow: 'hidden',
...Platform.select({ ios: { shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 }, android: { elevation: 2 } }),
},
leadingCard: { borderColor: '#22C55E', borderWidth: 2, backgroundColor: '#F0FDF4' },
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' },
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
liveBadge: { backgroundColor: '#FEE2E2' },
meBadge: { backgroundColor: '#BBF7D0' },
badgeText: { fontSize: 10, fontWeight: '900' },
liveText: { color: '#EF4444' },
meText: { color: '#15803D' },
arrow: { fontSize: 10, color: '#94A3B8' },
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
label: { fontSize: 11, color: '#94A3B8', textTransform: 'uppercase', fontWeight: '600' },
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' },
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: { width: 45, height: 45, backgroundColor: colors.primary, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
disabledBtn: { backgroundColor: '#CBD5E1' },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
inputBox: { flex: 1, height: 45, backgroundColor: '#F1F5F9', borderRadius: 12, flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
submitBtn: { height: 50, backgroundColor: colors.primary, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' },
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(255,255,255,0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 99 },
});
+18 -1
View File
@@ -15,4 +15,21 @@ export type allBooleanType = {
activeItemId: number | null
acitveItemData: any
timeEnd: boolean
}
}
export interface Props {
visible: boolean
setAllBoolean: any
allBoolean: any
template: any
}
export type SelectedItem = {
auctionItemId: number;
productImageName: string;
file: {
name: string;
uri: string;
type: string;
} | null;
};