Files
ProcureLinc-Mobile-App/src/Screens/Bidding/Emd.tsx
T

648 lines
22 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
StyleSheet,
TextInput,
Alert,
ActivityIndicator,
} 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 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/ApiCall/Hooks';
import { apiCall } from '../../Axios/Axios';
import {
AUCITON_SINGLE_EMD,
AUCTION_ITEM_SUBMIT,
CHECK_TECHNICAL_DOCUMENTAION,
GET_AUCTION_DETAILS,
GET_TECHNICAL_DOCUMENTAION,
} from '../../Services/ApiCall/ApiUrls';
import TemplateFormModal from './Component/TemplateFormModal';
import { SelectedItem } from './Type/BidType';
import { showSuccessAlert } from '../../Constant/ShowDailog';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import {
FileText,
Upload,
CheckCircle2,
ShieldCheck,
Paperclip,
ChevronRight,
PackageCheck,
BadgeIndianRupee,
} from 'lucide-react-native';
const Emd = () => {
const Navigation: any = useNavigation();
const { colors, spacing, shadows, typography } = useTheme();
const SelectedAuction = useSelector((state: any) => state.auction.auction);
const [selectedItems, setSelectedItems] = useState<SelectedItem[]>([]);
const [acceptedPolicy, setAcceptedPolicy] = useState(false);
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [allBoolean, setAllBoolean]: any = useState({
isSubmitEmdLoading: false,
isEmdComplete: false,
auctionItems: [],
technicalDocumentStatus: false,
selectTemplate: {},
technicalDocModalVisible: false,
emdType: false,
fileName: '',
file: {},
});
const { mutate } = useEmdCheck();
const { mutate: AuctionItemMutate, isPending } = useAuctionItem();
const isSelected = (id: number) => selectedItems.some(i => i.auctionItemId === id);
const toggleSelect = (item: any, checked: boolean) => {
if (!checked) {
setSelectedItems(prev => prev.filter(i => i.auctionItemId !== item.id));
} else {
setSelectedItems(prev => [
...prev,
{ auctionItemId: item.id, productImageName: '', file: null },
]);
}
};
const updateSelectedItem = (id: number, data: Partial<SelectedItem>) => {
setSelectedItems(prev =>
prev.map(i => (i.auctionItemId === id ? { ...i, ...data } : i)),
);
};
const pickFile = async (id: number) => {
try {
const res = await pick({ type: ['application/pdf'] });
const file = res[0];
updateSelectedItem(id, {
file: { name: file.name ?? 'document.pdf', uri: file.uri, type: 'application/pdf' },
});
} catch {
console.log('Pick cancelled');
}
};
const pickFileSingle = async () => {
try {
const res = await pick({ type: ['application/pdf'] });
const file = res[0];
setAllBoolean((prev: any) => ({
...prev,
file: { name: file.name ?? 'document.pdf', uri: file.uri, type: 'application/pdf' },
}));
} catch {
console.log('Pick cancelled');
}
};
/* ── Render Item ── */
const renderItem = ({ item }: any) => {
console.log(item, "item");
const selected = isSelected(item.id);
const selectedData = selectedItems.find(i => i.auctionItemId === item.id);
return (
<Animated.View
layout={Layout.springify()}
entering={FadeInDown.springify()}
style={[
styles.card,
selected && { borderColor: colors.primary, borderWidth: 1.5, backgroundColor: `${colors.primary}08` },
{ backgroundColor: selected ? `${colors.primary}08` : colors.surface },
]}
>
{/* Card Header */}
<View style={styles.cardHeader}>
<View style={styles.cardLeft}>
<View style={[styles.itemIconBox, { backgroundColor: `${colors.primary}15` }]}>
<PackageCheck size={20} color={colors.primary} strokeWidth={2} />
</View>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 14, fontWeight: '700', color: colors.text, marginBottom: 3 }}>
{item.productName}
</Text>
<View style={styles.emdRow}>
{/* <BadgeIndianRupee size={13} color={colors.primary} strokeWidth={2} /> */}
<Text style={{ fontSize: 12, fontWeight: '600', color: colors.primary }}>
EMD ₹ {item.emdAmount}
</Text>
</View>
</View>
</View>
<View style={{ width: 35 }}>
<BouncyCheckbox
size={22}
fillColor={colors.primary}
iconStyle={{ borderRadius: 7, borderWidth: 1.5, borderColor: colors.primary }}
innerIconStyle={{ borderRadius: 7 }}
isChecked={selected}
onPress={checked => toggleSelect(item, checked)}
/>
</View>
</View>
{/* Existing doc */}
<View style={[styles.existingDocRow, { backgroundColor: colors.overlay }]}>
<Paperclip size={13} color={colors.textSecondary} strokeWidth={2} />
<Text style={{ fontSize: 12, fontWeight: '400', color: colors.textSecondary }}>
Existing:{' '}
<Text style={{ fontSize: 12, fontWeight: '600', color: colors.text }}>
{item.itemDocumentName ?? 'Not Uploaded'}
</Text>
</Text>
</View>
{/* Expanded Form */}
{selected && (
<Animated.View entering={FadeInDown.springify()} exiting={FadeOutUp.duration(200)} style={styles.expandedForm}>
<View style={[styles.separator, { backgroundColor: colors.border }]} />
<TextInput
placeholder="Document name"
value={selectedData?.productImageName}
onChangeText={text => updateSelectedItem(item.id, { productImageName: text })}
style={[styles.textInput, { borderColor: colors.border, backgroundColor: colors.background, color: colors.text }]}
placeholderTextColor={colors.textSecondary}
/>
<TouchableOpacity
style={[styles.uploadBtn, { backgroundColor: colors.primary }]}
onPress={() => pickFile(item.id)}
activeOpacity={0.85}
>
<Upload size={15} color="#fff" strokeWidth={2.5} />
<Text style={{ fontSize: 12, fontWeight: '700', color: '#fff' }}>
{selectedData?.file ? 'Change PDF' : 'Upload PDF'}
</Text>
</TouchableOpacity>
{selectedData?.file && (
<View style={[styles.fileNameRow, { backgroundColor: `${colors.success}12` }]}>
<FileText size={13} color={colors.success} strokeWidth={2} />
<AppText variant="caption" color={colors.success} numberOfLines={1} style={{ flex: 1 } as any}>
{selectedData.file.name}
</AppText>
</View>
)}
</Animated.View>
)}
</Animated.View>
);
};
/* ── API calls (unchanged logic) ── */
const getAuctionItem = async () => {
try {
const payload = { auctionNumber: SelectedAuction.auctionNumber, status: SelectedAuction.auctionPattern };
AuctionItemMutate(payload, {
onSuccess: res => {
if (res.statusCode === 200) {
setAllBoolean((prev: any) => ({ ...prev, auctionItems: Array.isArray(res?.data) ? res.data : [res.data] }));
}
},
onError: () => { },
});
} catch (error) {
console.log('Auction fetch error', error);
}
};
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 res2 = await apiCall<any>('get', GET_TECHNICAL_DOCUMENTAION);
const auctionIdToFind = String(SelectedAuction.auctionId);
const matchedObject = res2.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);
}
};
const getAuction = async () => {
const selectedRes = await apiCall<any>('get', GET_AUCTION_DETAILS, {}, { id: SelectedAuction.auctionId });
const emdType = String(selectedRes.data.emd).trim().replace(/\s+/g, ' ').toLowerCase();
return selectedRes.statusCode === 200 && emdType === 'auction wise';
};
const AlreadyEmdCheck = async () => {
try {
setIsInitialLoading(true);
mutate(
{ auctionNumber: SelectedAuction.auctionNumber },
{
onSuccess: async res => {
try {
if (res.statusCode === 200) {
if (res.message === 'No EMD Documents') {
const result = await getAuction();
if (result) {
setAllBoolean((prev: any) => ({ ...prev, emdType: true }));
return;
}
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();
}
}
}
} finally {
setIsInitialLoading(false);
}
},
onError: () => {
setIsInitialLoading(false);
},
},
);
} catch (error) {
console.log('EMD Check Error:', error);
setIsInitialLoading(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 {
Navigation.navigate(SelectedAuction.auctionPattern === 'Forward' ? NavigationString.StartBidding : NavigationString.StartReverseBidding);
}
}
} catch (error) {
Alert.alert('Error', 'Failed to submit EMD');
} finally {
setAllBoolean((prev: any) => ({ ...prev, isSubmitEmdLoading: false }));
}
};
const EmdContinueHandle = async () => {
if (allBoolean.technicalDocumentStatus) {
Navigation.navigate(SelectedAuction.auctionPattern === 'Forward' ? NavigationString.StartBidding : NavigationString.StartReverseBidding);
} else {
getTemplateDocumentation();
}
};
const singleEmdSubmit = async () => {
try {
setAllBoolean((prev: any) => ({ ...prev, isSubmitEmdLoading: true }));
if (!acceptedPolicy) { Alert.alert('Policy Required', 'Please accept terms'); return; }
const formData = new FormData();
formData.append('auctionId', SelectedAuction.auctionId);
formData.append('productImageName', allBoolean.fileName);
formData.append('files', allBoolean.file);
const response = await apiCall<any>('post', AUCITON_SINGLE_EMD, formData, {}, 'multipart/form-data');
if (response?.statusCode === 200) {
showSuccessAlert(response.message || 'EMD submitted successfully');
Navigation.navigate(SelectedAuction.auctionPattern === 'Forward' ? NavigationString.StartBidding : NavigationString.StartReverseBidding);
}
} catch (error: any) {
console.log('Error:', error?.message || error);
} finally {
setAllBoolean((prev: any) => ({ ...prev, isSubmitEmdLoading: false }));
}
};
useEffect(() => { AlreadyEmdCheck(); }, []);
const styles = makeStyles(colors, spacing, shadows, typography);
/* ── Policy + Submit footer ── */
const BottomBar = ({ onSubmit }: { onSubmit: () => void }) => (
<View style={styles.bottomBar}>
<View style={styles.policyRow}>
<View style={{ width: 30 }}>
<BouncyCheckbox
size={20}
fillColor={colors.primary}
iconStyle={{ borderRadius: 6, borderWidth: 1.5, borderColor: colors.primary }}
innerIconStyle={{ borderRadius: 6 }}
isChecked={acceptedPolicy}
onPress={() => setAcceptedPolicy(p => !p)}
/>
</View>
<Text style={styles.policyText}>
I agree to{' '}
<Text style={{ fontWeight: '700', color: colors.primary }}>
Terms & Privacy Policy
</Text>
</Text>
</View>
<TouchableOpacity
style={[styles.submitBtn, !acceptedPolicy && styles.disabledBtn, { backgroundColor: acceptedPolicy ? colors.primary : colors.border }]}
onPress={onSubmit}
disabled={!acceptedPolicy}
activeOpacity={0.85}
>
{allBoolean.isSubmitEmdLoading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<>
<ShieldCheck size={18} color="#fff" strokeWidth={2.5} />
<Text style={styles.submitText}>
Submit EMD
</Text>
</>
)}
</TouchableOpacity>
</View>
);
return (
<View style={styles.root}>
<InsideHeader title="EMD Submission" showArrow />
{SelectedAuction.technicalDocReq && allBoolean.technicalDocModalVisible && (
<TemplateFormModal
visible={allBoolean.technicalDocModalVisible}
allBoolean={allBoolean}
setAllBoolean={setAllBoolean}
template={allBoolean.selectTemplate}
/>
)}
{(isPending || isInitialLoading) ? (
<Skelton />
) : allBoolean.isEmdComplete ? (
<EmdComplete onNext={EmdContinueHandle} />
) : allBoolean.emdType ? (
/* ── Single EMD (auction-wise) ── */
<View style={{ flex: 1 }}>
<View style={styles.singleEmdCard}>
{/* Icon banner */}
<View style={[styles.singleEmdBanner, { backgroundColor: `${colors.primary}10` }]}>
<ShieldCheck size={32} color={colors.primary} strokeWidth={1.5} />
<View>
<Text style={{ fontSize: 18, fontWeight: '800', color: colors.text }}>
Auction-wise EMD
</Text>
<Text style={{ fontSize: 12, fontWeight: '400', color: colors.textSecondary }}>
Upload a single document for this auction
</Text>
</View>
</View>
{/* Document name */}
<Text style={{ fontSize: 12, fontWeight: '700', color: colors.textSecondary, letterSpacing: 1, marginBottom: spacing.xs + 2 }}>
DOCUMENT NAME
</Text>
<TextInput
style={[styles.textInput, { borderColor: colors.border, backgroundColor: colors.background, color: colors.text, marginBottom: spacing.m }]}
placeholder="Enter document name…"
value={allBoolean.fileName}
placeholderTextColor={colors.textSecondary}
onChangeText={text => setAllBoolean((prev: any) => ({ ...prev, fileName: text }))}
/>
{/* File picker */}
<TouchableOpacity
style={[styles.uploadBtn, { backgroundColor: colors.primary }]}
onPress={pickFileSingle}
activeOpacity={0.85}
>
<Upload size={15} color="#fff" strokeWidth={2.5} />
<Text style={{ fontSize: 12, fontWeight: '700', color: '#fff' }}>
{allBoolean?.file?.name ? 'Change PDF' : 'Choose PDF'}
</Text>
</TouchableOpacity>
{allBoolean?.file?.name ? (
<View style={[styles.fileNameRow, { backgroundColor: `${colors.success}12`, marginTop: spacing.s }]}>
<FileText size={13} color={colors.success} strokeWidth={2} />
<Text style={{ fontSize: 12, fontWeight: '400', color: colors.success, flex: 1 }} numberOfLines={1}>
{allBoolean.file.name}
</Text>
</View>
) : null}
</View>
<BottomBar onSubmit={singleEmdSubmit} />
</View>
) : (
/* ── Item-wise EMD ── */
<View style={{ flex: 1 }}>
<FlatList
data={allBoolean.auctionItems}
keyExtractor={(item: any) => String(item.id)}
renderItem={renderItem}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
/>
<BottomBar onSubmit={handleSubmit} />
</View>
)}
</View>
);
};
export default Emd;
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any, typography?: any) =>
StyleSheet.create({
root: {
flex: 1,
backgroundColor: colors.background,
},
listContent: {
padding: spacing.m,
paddingBottom: 160,
},
/* ── Item card ── */
card: {
borderRadius: 18,
marginBottom: spacing.m,
padding: spacing.m,
borderWidth: 1,
borderColor: colors.border,
...shadows.light,
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: spacing.s,
},
cardLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
gap: spacing.s,
},
itemIconBox: {
width: 40,
height: 40,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
itemTitle: {
fontFamily: typography?.fonts?.bold,
marginBottom: 3,
} as any,
emdRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 3,
},
emdAmount: {
fontFamily: typography?.fonts?.bold,
} as any,
existingDocRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
marginTop: spacing.s,
paddingHorizontal: spacing.s + 2,
paddingVertical: 6,
borderRadius: 8,
},
expandedForm: {
marginTop: spacing.s,
},
separator: {
height: StyleSheet.hairlineWidth,
marginBottom: spacing.m,
},
textInput: {
borderWidth: 1.2,
borderRadius: 14,
padding: 12,
fontSize: 14,
marginBottom: spacing.s,
},
uploadBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 7,
paddingVertical: 13,
borderRadius: 14,
},
uploadText: {
fontFamily: typography?.fonts?.bold,
} as any,
fileNameRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
marginTop: spacing.s,
paddingHorizontal: spacing.s + 4,
paddingVertical: 8,
borderRadius: 10,
},
/* ── Single EMD card ── */
singleEmdCard: {
margin: spacing.m,
backgroundColor: colors.surface,
borderRadius: 20,
padding: spacing.m,
...shadows.light,
},
singleEmdBanner: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.m,
borderRadius: 14,
padding: spacing.m,
marginBottom: spacing.m,
},
fieldLabel: {
fontFamily: typography?.fonts?.bold,
letterSpacing: 1,
marginBottom: spacing.xs + 2,
} as any,
/* ── Bottom bar ── */
bottomBar: {
height: 160,
padding: spacing.m,
paddingBottom: spacing.m + 60,
backgroundColor: colors.surface,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.border,
...shadows.large,
},
policyRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
marginBottom: spacing.m,
},
policyText: {
flex: 1,
lineHeight: 20,
fontSize: 13,
fontWeight: '500',
color: colors.text,
} as any,
submitBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
paddingVertical: 15,
borderRadius: 16,
},
disabledBtn: {
opacity: 0.6,
},
submitText: {
fontSize: 14,
fontWeight: '800',
color: '#fff',
letterSpacing: 0.5,
textTransform: 'uppercase',
} as any,
});