diff --git a/src/Constant/NavigationString.tsx b/src/Constant/NavigationString.tsx index 72aecd0..ff2ed1e 100644 --- a/src/Constant/NavigationString.tsx +++ b/src/Constant/NavigationString.tsx @@ -9,6 +9,12 @@ const NavigationString = { Emd: 'Emd', StartBidding: 'StartBidding', PurchaseAuctionList:'PurchaseAuctionList', - StartReverseBidding:'StartReverseBidding' + StartReverseBidding:'StartReverseBidding', + // tender + PurchaseTenderList:'PurchaseTenderList', + ProceedToPurTender:'ProceedToPurTender', + TenderBidding:'TenderBidding', + ProceedToBidDetail:'ProceedToBidDetail', + LiveTenderDetail:'LiveTenderDetail', } export default NavigationString \ No newline at end of file diff --git a/src/Navigation/MainStack.jsx b/src/Navigation/MainStack.jsx index 3f3d987..85efda9 100644 --- a/src/Navigation/MainStack.jsx +++ b/src/Navigation/MainStack.jsx @@ -36,6 +36,11 @@ export default function MainStack() { + + + + + diff --git a/src/Redux/Slices/SelectedPurchaseTender.ts b/src/Redux/Slices/SelectedPurchaseTender.ts new file mode 100644 index 0000000..edd9b68 --- /dev/null +++ b/src/Redux/Slices/SelectedPurchaseTender.ts @@ -0,0 +1,39 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export interface TenderItem { + TenderNumber: string; + [key: string]: any; +} + +// ✅ DEFAULT = EMPTY ARRAY +const initialState: TenderItem[] = []; + +export const selectPurchaseTenderSlice = createSlice({ + name: 'selectPurchaseTender', + initialState, + reducers: { + togglePurchaseTender: ( + state, + action: PayloadAction + ) => { + const index = state.findIndex( + item => item.tenderId === action.payload.tenderId + ); + + if (index !== -1) { + state.splice(index, 1); + } else { + state.push(action.payload); + } + }, + + clearPurchaseTender: () => [], + }, +}); + +export const { + togglePurchaseTender, + clearPurchaseTender, +} = selectPurchaseTenderSlice.actions; + +export default selectPurchaseTenderSlice.reducer; diff --git a/src/Redux/Slices/SelectedTender.ts b/src/Redux/Slices/SelectedTender.ts new file mode 100644 index 0000000..ca7e58a --- /dev/null +++ b/src/Redux/Slices/SelectedTender.ts @@ -0,0 +1,23 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + tender: '', +}; + +export const tenderSlice = createSlice({ + name: 'tender', + initialState, + reducers: { + setTender: (state, { payload }) => { + state.tender = payload; + }, + removeTender: (state) => { + state.tender = ''; + }, + setTenderDetails: (state, { payload }) => { + state.tender = payload; + }, + }, +}); +export const { setTender , removeTender, setTenderDetails } = tenderSlice.actions; +export default tenderSlice.reducer; diff --git a/src/Redux/Store.js b/src/Redux/Store.js index d9be3ce..a475847 100644 --- a/src/Redux/Store.js +++ b/src/Redux/Store.js @@ -5,11 +5,15 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import {tokenSlice} from './Slices/Token'; import {auctionSlice} from './Slices/SelectedAuction'; import selectPurchaseAuctionReducer from './Slices/selectPurchaseAuction' +import selectPurchaseTenderReducer from './Slices/SelectedPurchaseTender'; +import { tenderSlice } from './Slices/SelectedTender'; const rootReducer = combineReducers({ token: tokenSlice.reducer, auction: auctionSlice.reducer, selectPurchaseAuction: selectPurchaseAuctionReducer, + selectPurchaseTender: selectPurchaseTenderReducer, + tender: tenderSlice.reducer, }); const persistConfig = { key: 'root', diff --git a/src/Screens/Bidding/Emd.tsx b/src/Screens/Bidding/Emd.tsx index 613b940..2b168b4 100644 --- a/src/Screens/Bidding/Emd.tsx +++ b/src/Screens/Bidding/Emd.tsx @@ -93,14 +93,14 @@ const Emd = () => { EMD ₹ {item.emdAmount} - toggleSelect(item, checked)} - /> + /> */} diff --git a/src/Screens/Login/Login.tsx b/src/Screens/Login/Login.tsx index 4ac7a9a..4bb4af1 100644 --- a/src/Screens/Login/Login.tsx +++ b/src/Screens/Login/Login.tsx @@ -38,6 +38,7 @@ const Login = () => { mutate({ password: values.password, emailAdd: values.email }, { onSuccess: (res) => { if (res.statusCode == 200) { + console.log("login token : ", res) Dispatch(setToken({ ...res.data, token: res.token })) Navigation.dispatch( CommonActions.reset({ diff --git a/src/Screens/Tender/LiveTender.tsx b/src/Screens/Tender/LiveTender.tsx new file mode 100644 index 0000000..3e48882 --- /dev/null +++ b/src/Screens/Tender/LiveTender.tsx @@ -0,0 +1,139 @@ +import React, { useCallback, useState, useEffect } from 'react'; +import { View, FlatList, ActivityIndicator, RefreshControl, Text } from 'react-native'; +import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated'; +import { useNavigation } from '@react-navigation/native'; +import { useFetchLiveAuction, useFetchLiveTender } from '../../Services/BBPS/Hooks'; +import { SearchInput } from '../../Component/Serach/SeacrhInput'; +import { Skelton } from '../../Component/Index'; +import colors from '../../Constant/Color'; +import { useDispatch, useSelector } from 'react-redux'; +import { togglePurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender'; +import LiveTenderTile from './LiveTenderTile'; + +const LiveTender = () => { + const navigation: any = useNavigation(); + const Dispatch = useDispatch() + const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender); + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch }: any = useFetchLiveTender(); + const [showRefreshBanner, setShowRefreshBanner] = useState(false); + + useEffect(() => { + if (isRefetching) { + setShowRefreshBanner(true); + } else { + const timer = setTimeout(() => { + setShowRefreshBanner(false); + }, 1000); + + return () => clearTimeout(timer); + } + }, [isRefetching]); + + const loadMore = useCallback(() => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, [hasNextPage, isFetchingNextPage]); + + const renderFooter = useCallback(() => { + if (!isFetchingNextPage) return null; + return ( + + + + ); + }, [isFetchingNextPage]); + + const renderItem = useCallback( + ({ item, index }: any) => { + const isPurchase = SelectedPurchaseList.findIndex((prev: any) => prev.tenderId === item.tenderId) + return { Dispatch(togglePurchaseTender(item)) }} + // onDetailsPress={() => navigation.navigate('LiveTenderDetail', {item})} + /> + }, + [SelectedPurchaseList] + ); + + if (isLoading) { + return ; + } + + return ( + + { }} placeholder="Search...." /> + + {showRefreshBanner && ( + + + + Refreshing auctions + + + )} + + String(item.auctionId)} + renderItem={renderItem} + showsVerticalScrollIndicator={false} + contentContainerStyle={{ paddingBottom: 50, paddingTop: 10 }} + onEndReached={loadMore} + onEndReachedThreshold={0.2} + ListFooterComponent={renderFooter} + refreshControl={ + + } + initialNumToRender={6} + maxToRenderPerBatch={6} + windowSize={7} + removeClippedSubviews + updateCellsBatchingPeriod={50} + /> + + ); +}; + +export default LiveTender; \ No newline at end of file diff --git a/src/Screens/Tender/LiveTenderDetail.tsx b/src/Screens/Tender/LiveTenderDetail.tsx new file mode 100644 index 0000000..5cd345a --- /dev/null +++ b/src/Screens/Tender/LiveTenderDetail.tsx @@ -0,0 +1,18 @@ +import { StyleSheet, Text, View } from 'react-native' +import React from 'react' +import { useRoute } from '@react-navigation/native'; + +const LiveTenderDetail = () => { + const route = useRoute(); + const { tender } = route.params; + console.log("live tender details : ", tender) + return ( + + LiveTenderDetail + + ) +} + +export default LiveTenderDetail + +const styles = StyleSheet.create({}) \ No newline at end of file diff --git a/src/Screens/Tender/LiveTenderTile.tsx b/src/Screens/Tender/LiveTenderTile.tsx new file mode 100644 index 0000000..bc5104c --- /dev/null +++ b/src/Screens/Tender/LiveTenderTile.tsx @@ -0,0 +1,246 @@ +import React, { memo } from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import moment from 'moment'; +import colors from '../../Constant/Color'; +import { LiveTenderTileProps } from './types/types'; +import { Calendar, Tag, Info, ArrowRight } from 'lucide-react-native'; +import TenderDetail from './Modal/TenderFullDetail'; +import TenderFullDetail from './Modal/TenderFullDetail'; +import { useNavigation } from '@react-navigation/native'; + +const LiveTenderTile: React.FC = ({ + data, + isPurchased = false, + onPurchasePress, + tenderDispatchDate, + tenderOfWork, + purchaseEnd, + tenderFees, + tenderNumber, + onDetailsPress +}) => { + const navigation: any = useNavigation(); + const purchaseEnded = moment().isAfter(moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss')); + + console.log("live tender data : ", data) + return ( + + {/* Status Tag */} + + + navigation.navigate('LiveTenderDetail', data)} + > + {/* Header */} + + + + #{tenderNumber} + + ₹{tenderFees} + + + {/* Work Title */} + + {tenderOfWork ?? 'General Tender Work'} + + + {/* Info Grid */} + + + + + Start + {tenderDispatchDate ?? '-'} + + + + + + + Deadline + + {moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, YY')} + + + + + + {/* Action Bar */} + + + + Details + + + + + {purchaseEnded ? 'Expired' : isPurchased ? 'Remove' : 'Add to Cart'} + + {!purchaseEnded && ( + + )} + + + + {/* Tender Detail Modal */} + {/* setShowDetail(false)} /> */} + + + ); +}; + +export default memo(LiveTenderTile); + +const styles = StyleSheet.create({ + cardWrapper: { + marginVertical: 10, + marginHorizontal: 16, + backgroundColor: '#fff', + borderRadius: 24, + flexDirection: 'row', + overflow: 'hidden', + + // Modern shadow + shadowColor: '#000', + shadowOffset: { width: 0, height: 10 }, + shadowOpacity: 0.08, + shadowRadius: 20, + elevation: 8, + }, + statusTag: { + width: 6, + height: '100%', + }, + container: { + flex: 1, + padding: 20, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + }, + badge: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.primary + '25', + paddingHorizontal: 12, + paddingVertical: 5, + borderRadius: 12, + gap: 6, + }, + badgeText: { + fontSize: 12, + fontWeight: '700', + color: colors.primary, + }, + priceText: { + fontSize: 20, + fontWeight: '800', + color: '#0f172a', + }, + workTitle: { + fontSize: 16, + fontWeight: '600', + color: '#334155', + lineHeight: 24, + marginBottom: 16, + }, + infoGrid: { + flexDirection: 'row', + justifyContent: 'space-between', + backgroundColor: '#f0f4f8', + paddingVertical: 12, + paddingHorizontal: 16, + borderRadius: 16, + marginBottom: 18, + }, + infoItem: { + flexDirection: 'row', + alignItems: 'center', + gap: 10, + }, + infoTextGroup: { + flexDirection: 'column', + }, + infoLabel: { + fontSize: 10, + color: '#94a3b8', + textTransform: 'uppercase', + fontWeight: '700', + }, + infoValue: { + fontSize: 13, + fontWeight: '700', + color: '#1e293b', + }, + actionBar: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + gap: 12, + }, + iconBtn: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingHorizontal: 10, + paddingVertical: 6, + borderRadius: 10, + backgroundColor: '#e2e8f0', + }, + iconBtnText: { + fontSize: 14, + fontWeight: '700', + color: colors.primary, + }, + mainButton: { + flex: 1, + height: 46, + borderRadius: 16, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + }, + btnActive: { + backgroundColor: '#4ade80', // light green + }, + btnPurchased: { + backgroundColor: colors.error, + borderWidth: 2, + borderColor: colors.error, + }, + btnDisabled: { + backgroundColor: '#e2e8f0', + }, + mainButtonText: { + fontSize: 14, + fontWeight: '800', + }, +}); diff --git a/src/Screens/Tender/Modal/FilterModal.tsx b/src/Screens/Tender/Modal/FilterModal.tsx new file mode 100644 index 0000000..fa9094c --- /dev/null +++ b/src/Screens/Tender/Modal/FilterModal.tsx @@ -0,0 +1,259 @@ +import React, { useState } from 'react'; +import { + Modal, + View, + Text, + StyleSheet, + TouchableOpacity, + Pressable, +} from 'react-native'; +import DatePicker from 'react-native-date-picker'; +import moment from 'moment'; +import colors from '../../../Constant/Color'; +import FontsFamily from '../../../Constant/FontsFamily'; + +type AuctionStatus = 'RUNNING' | 'ENDED' | 'NOT_STARTED' | null; + +interface Props { + visible: boolean; + onClose: () => void; + onApply: (filters: { + fromDate: Date | null; + toDate: Date | null; + status: AuctionStatus; + }) => void; +} + +const FilterModal: React.FC = ({ visible, onClose, onApply }) => { + const [fromDate, setFromDate] = useState(null); + const [toDate, setToDate] = useState(null); + const [status, setStatus] = useState(null); + + const [openPicker, setOpenPicker] = useState(false); + const [pickerType, setPickerType] = useState<'FROM' | 'TO'>('FROM'); + + const openDatePicker = (type: 'FROM' | 'TO') => { + setPickerType(type); + setOpenPicker(true); + }; + + const handleConfirm = (date: Date) => { + if (pickerType === 'FROM') { + setFromDate(date); + } else { + setToDate(date); + } + setOpenPicker(false); + }; + + const handleApply = () => { + onApply({ fromDate, toDate, status }); + onClose(); + }; + + return ( + <> + + {/* Overlay */} + + + {/* Content */} + + Filter Auctions + + {/* ===== DATE RANGE ===== */} + Date Range + + + openDatePicker('FROM')} + > + From + + {fromDate + ? moment(fromDate).format('DD MMM YYYY') + : 'Select date'} + + + + openDatePicker('TO')} + > + To + + {toDate + ? moment(toDate).format('DD MMM YYYY') + : 'Select date'} + + + + + {/* ===== STATUS ===== */} + Auction Status + + + {[ + { label: 'Running', value: 'RUNNING' }, + { label: 'Ended', value: 'ENDED' }, + { label: 'Not Started', value: 'NOT_STARTED' }, + ].map(item => { + const active = status === item.value; + return ( + setStatus(item.value as AuctionStatus)} + style={[ + styles.statusPill, + active && styles.statusActive, + ]} + > + + {item.label} + + + ); + })} + + + {/* ===== FOOTER ===== */} + + {/* + Cancel + */} + + + Apply + + + + + + {/* ===== DATE PICKER ===== */} + setOpenPicker(false)} + /> + + ); +}; + +export default FilterModal; + + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.4)', + }, + container: { + position: 'absolute', + bottom: 0, + width: '100%', + backgroundColor: '#fff', + borderTopLeftRadius: 22, + borderTopRightRadius: 22, + padding: 20, + }, + title: { + fontSize: 18, + fontWeight: '700', + marginBottom: 20, + }, + sectionTitle: { + fontSize: 14, + fontWeight: '600', + marginBottom: 10, + color: '#475569', + }, + dateRow: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 20, + }, + dateBox: { + width: '48%', + padding: 14, + borderRadius: 12, + backgroundColor: '#F8FAFC', + borderWidth: 1, + borderColor: '#E2E8F0', + }, + dateLabel: { + fontSize: 11, + color: '#64748B', + marginBottom: 4, + }, + dateValue: { + fontSize: 14, + fontWeight: '600', + color: '#0F172A', + }, + statusRow: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 10, + }, + statusPill: { + paddingVertical: 10, + paddingHorizontal: 14, + borderRadius: 20, + borderWidth: 1, + borderColor: '#CBD5E1', + backgroundColor: '#F8FAFC', + }, + statusActive: { + backgroundColor: colors.primary, + borderColor: colors.primary, + }, + statusText: { + fontSize: 13, + color: '#334155', + fontFamily:FontsFamily.poppinsSemiBold + }, + statusActiveText: { + color: '#fff', + }, + footer: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 24, + }, + cancelBtn: { + padding: 12, + }, + + applyBtn: { + backgroundColor: colors.primary, + paddingHorizontal: 20, + paddingVertical: 12, + borderRadius: 12, + width: '100%' + }, + applyText: { + color: '#fff', + fontSize: 16, + textAlign: 'center', + fontFamily: FontsFamily.poppinsSemiBold + }, +}); diff --git a/src/Screens/Tender/Modal/TenderFullDetail.tsx b/src/Screens/Tender/Modal/TenderFullDetail.tsx new file mode 100644 index 0000000..eaa78e8 --- /dev/null +++ b/src/Screens/Tender/Modal/TenderFullDetail.tsx @@ -0,0 +1,370 @@ +import React, { memo, useState } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + ScrollView, + SafeAreaView, + Platform, + StatusBar, +} from 'react-native'; +import Modal from 'react-native-modal'; +import moment from 'moment'; +import { + Briefcase, + Calendar, + FileText, + Download, + Eye, + Layers, + ArrowLeft, + Clock, +} from 'lucide-react-native'; +import colors from '../../../Constant/Color'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useFetchTenderById } from '../../../Services/BBPS/Hooks'; + +interface Props { + visible: boolean; + onClose: () => void; + data?: any; +} + +const TenderFullDetail: React.FC = ({ visible, onClose, data }) => { + + + const inset = useSafeAreaInsets(); + const { data: tenderResponse }: any = useFetchTenderById(data?.tenderId); + const tender = tenderResponse?.data?.[0]; + + const formatDate = (date?: string) => { + if (!date) return '-'; + return moment(date, 'DD-MM-YYYY HH:mm:ss').format('DD MMM YYYY, hh:mm A'); + }; + + if (!tender) return null; + + const isLive = tender?.liveStatus === true; + + const Section = ({ title, icon: Icon, children }: any) => ( + + + + {title} + + {children} + + ); + + const InfoBox = ({ label, value, full = false }: any) => ( + + {label} + {value || '-'} + + ); + + return ( + + + + + {/* Header */} + + + + + + + Tender Overview + Ref: {tender?.tenderNumber} + + + + + {isLive ? 'LIVE' : 'CLOSED'} + + + + + + {/* Basic Details */} +
+ + + + + + + +
+ + {/* Work & Estimate */} +
+ + + + + + + + +
+ + {/* Department & Location */} +
+ + + + + + +
+ + {/* Fee & Charges */} +
+ + + + + + + +
+ + {/* Bid Info */} +
+ + + + + + + +
+ + {/* Key Dates */} +
+ + + + + + +
+ + {/* Documents */} +
+ {tender?.tenderDocuments?.map((doc: any) => ( + + + {doc.fileName} + + + ))} +
+ +
+
+
+ ); +}; + +const styles = StyleSheet.create({ + fullModal: { + margin: 0, + backgroundColor: '#fff', + }, + safeArea: { + flex: 1, + backgroundColor: '#fff', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + borderBottomWidth: 1, + borderBottomColor: '#f1f5f9', + backgroundColor: '#fff', + }, + backBtn: { + padding: 8, + marginRight: 8, + }, + headerTitleContainer: { + flex: 1, + }, + headerTitleText: { + fontSize: 18, + fontWeight: '800', + color: '#0f172a', + }, + headerIdText: { + fontSize: 13, + color: '#64748b', + }, + liveIndicator: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#f8fafc', + paddingHorizontal: 10, + paddingVertical: 5, + borderRadius: 20, + gap: 6, + borderWidth: 1, + borderColor: '#e2e8f0', + }, + dot: { width: 8, height: 8, borderRadius: 4 }, + liveText: { fontSize: 11, fontWeight: '800', color: '#475569' }, + + scrollView: { flex: 1, backgroundColor: '#f8fafc' }, + scrollContent: { padding: 16, paddingBottom: 100 }, + + sectionContainer: { + backgroundColor: '#fff', + borderRadius: 16, + padding: 16, + marginBottom: 16, + borderWidth: 1, + borderColor: '#eef2f6', + }, + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', + gap: 10, + marginBottom: 20, + paddingBottom: 10, + borderBottomWidth: 1, + borderBottomColor: '#f8fafc', + }, + sectionTitle: { + fontSize: 14, + fontWeight: '800', + color: '#1e293b', + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + grid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + }, + infoBox: { marginBottom: 16 }, + label: { fontSize: 12, color: '#94a3b8', fontWeight: '600', marginBottom: 4 }, + value: { fontSize: 15, fontWeight: '700', color: '#334155' }, + spacer: { height: 12 }, + + timelineContainer: { paddingLeft: 8 }, + timelineRow: { flexDirection: 'row', alignItems: 'center', gap: 15 }, + iconCircle: { width: 34, height: 34, borderRadius: 10, backgroundColor: '#f1f5f9', alignItems: 'center', justifyContent: 'center' }, + timelineConnector: { width: 2, height: 20, backgroundColor: '#f1f5f9', marginLeft: 16 }, + + tableWrapper: { backgroundColor: '#f8fafc', borderRadius: 12, padding: 12 }, + tableHead: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: '#e2e8f0', paddingBottom: 8, marginBottom: 12 }, + tableLabel: { fontSize: 11, fontWeight: '800', color: '#94a3b8', flex: 1 }, + tableRow: { flexDirection: 'row', alignItems: 'center' }, + itemMain: { fontSize: 14, fontWeight: '700', color: '#1e293b' }, + itemSub: { fontSize: 12, color: '#64748b' }, + itemQty: { flex: 1, fontSize: 14, fontWeight: '700', color: '#334155' }, + actionIcon: { padding: 6 }, + + fileCard: { flexDirection: 'row', alignItems: 'center', gap: 12, backgroundColor: '#f8fafc', padding: 12, borderRadius: 12 }, + fileIcon: { width: 40, height: 40, borderRadius: 8, backgroundColor: '#fee2e2', alignItems: 'center', justifyContent: 'center' }, + fileName: { fontSize: 14, fontWeight: '700', color: '#334155' }, + fileSize: { fontSize: 12, color: '#94a3b8' }, + + footer: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + backgroundColor: '#fff', + padding: 20, + paddingBottom: Platform.OS === 'ios' ? 34 : 20, + borderTopWidth: 1, + borderTopColor: '#f1f5f9', + }, + mainBtn: { + backgroundColor: colors.primary, + height: 56, + borderRadius: 16, + alignItems: 'center', + justifyContent: 'center', + shadowColor: colors.primary, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.2, + shadowRadius: 8, + elevation: 4, + }, + btnDisabled: { backgroundColor: '#cbd5e1' }, + mainBtnText: { color: '#fff', fontSize: 16, fontWeight: '800' }, + fileRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + paddingVertical: 10, + borderBottomWidth: 1, + borderBottomColor: '#f1f5f9', + width: '80%', + }, + liveBox: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingHorizontal: 10, + paddingVertical: 5, + borderRadius: 20, + borderWidth: 1, + borderColor: '#e2e8f0', + }, + headerSub: { + fontSize: 13, + color: '#64748b', + }, + headerTitle: { + fontSize: 18, + fontWeight: '800', + color: '#0f172a', + }, +}); + +export default memo(TenderFullDetail); \ No newline at end of file diff --git a/src/Screens/Tender/ProceedToBidDetail.tsx b/src/Screens/Tender/ProceedToBidDetail.tsx new file mode 100644 index 0000000..086ef56 --- /dev/null +++ b/src/Screens/Tender/ProceedToBidDetail.tsx @@ -0,0 +1,553 @@ +import { StyleSheet, Text, View, ScrollView, StatusBar, TouchableOpacity } from 'react-native' +import React from 'react' +import { InsideHeader } from '../../Component/Index' +import colors from '../../Constant/Color' +import { useNavigation, useRoute } from '@react-navigation/native' +import { useProceedToBid } from '../../Services/BBPS/Hooks' + +const ProceedToBidDetail = () => { + // Sample data based on your JSON + const route = useRoute() + const tenderData = route.params?.tender; + const {data, refetch} = useProceedToBid(tenderData?.tenderId); + + const navigation = useNavigation() + // Format date from array + + const handleStartBidding = () => { + refetch(); + if (data) { + navigation.navigate('TenderBidding', { tenderData: tenderData , bidData: data.data }); + } + } + const formatDate = (dateArray: any[]) => { + if (!dateArray || dateArray.length < 3) return "N/A"; + const [year, month, day, hour = 0, minute = 0] = dateArray; + return `${day.toString().padStart(2, '0')}/${month.toString().padStart(2, '0')}/${year} ${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; + }; + + // Status indicator component + const StatusIndicator = ({ status, label }) => ( + + + + {label}: {status ? "Submitted" : "Not Submitted"} + + + ); + + // Envelope log card component + const EnvelopeLogCard = ({ log, title }) => ( + + + {title} + + {log?.envelope} + + + + + Activity: + {log?.activity} + + + + Date & Time: + {log?.dateTime} + + + + System Info: + {log?.osName} / {log?.browserName} + + + + IP Address: + {log?.ipAddress} + + + + Status Message: + {log?.logMessage} + + + {log?.headingMsg && ( + + Heading: + {log?.headingMsg} + + )} + + ); + + // Information row component + const InfoRow = ({ label, value, isHighlighted = false }) => ( + + {label}: + {value} + + ); + + // Feature chip component + const FeatureChip = ({ label, enabled }) => ( + + + {label} {enabled ? '✓' : '✗'} + + + ); + + return ( + + + + + + {/* Tender Header Section */} + + + + {tenderData.tenderNumber} + ID: {tenderData.tenderId} + + + + {tenderData.biddingStatus ? "Active" : "Inactive"} + + + + + {tenderData.nameofWork} + + + + NIT No. + {tenderData.nitNo} + + + + EMD Amount + ₹{tenderData.emdInFig} + + + + + {/* Envelope Submission Status */} + + Envelope Submission Status + + + + + + + + {/* Envelope Logs */} + {tenderData.envelopeA_Logs && ( + + Submission Logs + + + + + )} + + {/* Tender Details */} + + Tender Details + + + + + + + + + + {/* Time Period */} + + Submission Time Period + + + Starts On: + {formatDate(tenderData.submittedStartDate)} + + + Ends On: + {formatDate(tenderData.submittedEndDate)} + + + + + {/* Feature Flags */} + + Available Features + + + + + + + + + + + {/* Final Status */} + + Overall Status + + All Envelopes Submitted: + + {tenderData.envelopeASubmittStatus && tenderData.envelopeBSubmittStatus && tenderData.envelopeCSubmittStatus + ? "COMPLETED" : "PENDING"} + + + + Bidding Status: + + {tenderData.biddingStatus ? "ACTIVE" : "CLOSED"} + + + + + + + + + + + { + tenderData?.proceedtoBid && !tenderData?.viewBid && tenderData?.biddingStatus && tenderData?.bidWithdrawStatus === false ? ( + + + Start Bidding + + + ) : One time bidding is completed + } + + + ) +} + +export default ProceedToBidDetail + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + marginBottom: 60, + }, + headerCard: { + backgroundColor: colors.primaryLight, + borderRadius: 12, + padding: 20, + marginBottom: 20, + borderLeftWidth: 4, + borderLeftColor: colors.primary, + }, + tenderHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + }, + tenderNumber: { + fontSize: 22, + fontWeight: 'bold', + color: colors.primary, + }, + tenderId: { + fontSize: 14, + color: colors.darkGray, + marginTop: 4, + }, + statusBadge: { + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 20, + }, + statusBadgeText: { + color: colors.white, + fontWeight: '600', + fontSize: 12, + }, + workDescription: { + fontSize: 16, + color: colors.darkText, + lineHeight: 22, + marginBottom: 16, + }, + headerInfoRow: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.white, + borderRadius: 8, + padding: 12, + }, + headerInfoItem: { + flex: 1, + alignItems: 'center', + }, + headerInfoLabel: { + fontSize: 12, + color: colors.darkGray, + marginBottom: 4, + }, + headerInfoValue: { + fontSize: 16, + fontWeight: '600', + color: colors.primary, + }, + separator: { + width: 1, + height: 30, + backgroundColor: colors.lightGray, + }, + section: { + marginBottom: 20, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '600', + color: colors.primary, + marginBottom: 12, + paddingLeft: 4, + }, + statusGrid: { + backgroundColor: colors.white, + borderRadius: 12, + padding: 16, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + statusContainer: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 12, + }, + statusDot: { + width: 12, + height: 12, + borderRadius: 6, + marginRight: 10, + }, + statusText: { + fontSize: 15, + fontWeight: '500', + }, + envelopeCard: { + backgroundColor: colors.white, + borderRadius: 12, + padding: 16, + marginBottom: 12, + borderWidth: 1, + borderColor: colors.lightGray, + }, + cardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + }, + cardTitle: { + fontSize: 16, + fontWeight: '600', + color: colors.primary, + }, + envelopeBadge: { + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + }, + envelopeBadgeText: { + color: colors.white, + fontWeight: 'bold', + fontSize: 12, + }, + logDetailRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + label: { + fontSize: 14, + color: colors.darkGray, + flex: 1, + }, + value: { + fontSize: 14, + color: colors.darkText, + fontWeight: '500', + flex: 2, + textAlign: 'right', + }, + successText: { + color: colors.success, + fontWeight: '600', + }, + detailsCard: { + backgroundColor: colors.white, + borderRadius: 12, + padding: 16, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + infoRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 10, + borderBottomWidth: 1, + borderBottomColor: colors.lightestGray, + }, + infoLabel: { + fontSize: 14, + color: colors.darkGray, + }, + infoValue: { + fontSize: 15, + fontWeight: '500', + color: colors.darkText, + }, + highlightedValue: { + color: colors.success, + fontWeight: '600', + }, + timeCard: { + backgroundColor: colors.white, + borderRadius: 12, + padding: 16, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + timeRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: colors.lightestGray, + }, + timeLabel: { + fontSize: 15, + color: colors.darkGray, + fontWeight: '500', + }, + timeValue: { + fontSize: 15, + color: colors.primary, + fontWeight: '600', + }, + chipsContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 10, + }, + chip: { + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 20, + marginBottom: 8, + }, + chipText: { + fontSize: 14, + fontWeight: '500', + }, + finalStatusCard: { + backgroundColor: colors.primaryLight, + borderRadius: 12, + padding: 20, + marginTop: 10, + borderWidth: 2, + borderColor: colors.primary, + }, + finalStatusTitle: { + fontSize: 18, + fontWeight: 'bold', + color: colors.primary, + marginBottom: 16, + textAlign: 'center', + }, + finalStatusRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + paddingVertical: 8, + borderBottomWidth: 1, + borderBottomColor: 'rgba(0, 0, 0, 0.1)', + }, + finalStatusLabel: { + fontSize: 16, + color: colors.darkText, + fontWeight: '500', + }, + finalStatusValue: { + fontSize: 16, + fontWeight: 'bold', + }, + footerSpace: { + height: 30, + }, + success: { + color: colors.success, + }, + error: { + color: colors.error, + }, + primary: { + color: colors.primary, + }, + warning: { + color: colors.warning, + }, + successLight: { + backgroundColor: colors.successLight, + }, + lightGray: { + backgroundColor: colors.lightGray, + }, + footer: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + backgroundColor: colors.white, + padding: 16, + borderTopWidth: 1, + borderTopColor: colors.lightGray, + elevation: 10, + }, + proceedButton: { + backgroundColor: colors.primary, + paddingVertical: 14, + borderRadius: 30, + alignItems: 'center', + }, + proceedButtonText: { + color: colors.white, + fontSize: 16, + fontWeight: '600', + }, + footerText: { + textAlign: 'center', + color: colors.darkGray, + fontSize: 14, + }, +}); \ No newline at end of file diff --git a/src/Screens/Tender/ProceedToPurTender.tsx b/src/Screens/Tender/ProceedToPurTender.tsx new file mode 100644 index 0000000..133e099 --- /dev/null +++ b/src/Screens/Tender/ProceedToPurTender.tsx @@ -0,0 +1,247 @@ +import React, { useState } from 'react'; +import { View, Text, FlatList, StyleSheet, TouchableOpacity, Alert } from 'react-native'; +import { InsideHeader, Loader } from '../../Component/Index'; +import colors from '../../Constant/Color'; +import { CreditCard, Calendar, User, DollarSign, ArrowRight } from 'lucide-react-native'; +import { useSelector } from 'react-redux'; +import { useOfflineTenderPurchase } from '../../Services/BBPS/Hooks'; +import { useNavigation } from '@react-navigation/native'; +import NavigationString from '../../Constant/NavigationString'; + +const ProceedToPurTender = () => { + const navigation: any = useNavigation(); + const [loading, setLoading] = useState(false); + const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender); + + const { mutate, isPending } = useOfflineTenderPurchase() + const calculateTotalAmount = (item: any) => { + const gstAmount = + (Number(item.documentFees) + Number(item.portalCharge)) * + (Number(item.gst) / 100); + + const payableAmount = + Number(item.documentFees) + Number(item.portalCharge) + gstAmount; + return payableAmount + } + + const handleOfflinePurchase = async () => { + const orderNo = `ORD${Date.now()}`; + const body = { + tenderDetails: SelectedPurchaseList.map((item: any, index:number) => ({ + gst: item.gst, + regId: item.regId || 0, + deptId: item.deptId || 1, + gstAmount: item.gst / 100, + payableAmount: calculateTotalAmount(item), + portalCharge: item.portalCharge, + tenderFees: item.documentFees, + tenderId: item.tenderId, + tenderNumber: item.tenderNumber, + tenderStatus: item.tenderStatus ?? "", + tenderType: item.tenderType ?? "", + isPurchased: item.purchaseStatus ?? false, + totalPayableAmount: calculateTotalAmount(item), + percentageAmount: calculateTotalAmount(item) / 100, + newTenderDocumentFees: item.documentFees * index + 1, + })), + orderNo: orderNo, + }; + + console.log("tender offline body : ", body) + try { + mutate(body, { + onSuccess: (res) => { + console.log(res, '---------------------purchsase response'); + Alert.alert('Offline Purchase Successful!', `Order No: ${orderNo}`); + navigation.goBack() + }, + onError: (error) => { + console.log(error, "================purchse Error"); + } + }) + + } catch (error) { + console.log("purchase offline error", error); + } + } + + const handlePurchase = (type: 'online' | 'offline') => { + setLoading(true); + setTimeout(() => { + setLoading(false); + type === 'online' ? Alert.alert('Online Purchase Successful!') : handleOfflinePurchase(); + + }, 1200); + }; + + const renderItem = ({ item }: { item: any[0] }) => ( + + + {item.auctionNumber} + {item.auctionPattern} + + + + + End Date: {item.purchaseEnd} + + + + + Tender No: {item.tenderNumber} + + + + + Tender Fees: ₹{item.documentFees} + + + + + GST ({item.gst}%): ₹{item.gst / 100} + + + + + Portal Charge: ₹{item.portalCharge} + + + + + Total: ₹{calculateTotalAmount(item)} + + + ); + + return ( + + + + item.tenderId.toString()} + contentContainerStyle={styles.container} + /> + + + + handlePurchase('online')} + > + Purchase Online + + + handlePurchase('offline')} + > + Purchase Offline + + + + + {loading && ( + + + + )} + + ); +}; + +export default ProceedToPurTender; + +const styles = StyleSheet.create({ + screen: { + flex: 1, + }, + container: { + padding: 16, + paddingBottom: 120, + }, + card: { + backgroundColor: '#fff', + borderRadius: 16, + padding: 16, + marginBottom: 16, + elevation: 5, + shadowColor: '#000', + shadowOpacity: 0.12, + shadowOffset: { width: 0, height: 3 }, + shadowRadius: 8, + }, + cardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 12, + }, + title: { + fontSize: 16, + fontWeight: '700', + color: '#0F172A', + }, + pattern: { + fontSize: 14, + color: colors.primary, + fontWeight: '600', + }, + cardRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + marginBottom: 4, + }, + text: { + fontSize: 14, + color: '#555', + }, + totalRow: { + marginTop: 10, + backgroundColor: colors.primary, + borderRadius: 12, + paddingVertical: 10, + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + gap: 8, + }, + total: { + fontSize: 15, + fontWeight: '700', + color: '#fff', + }, + bottomButtonContainer: { + position: 'absolute', + bottom: 0, + left: 0, + flexDirection: 'row', + backgroundColor: colors.white, + width: '100%', + paddingVertical: 20, + paddingHorizontal: 10, + elevation: 5 + }, + button: { + flex: 1, + backgroundColor: colors.primary, + paddingVertical: 14, + borderRadius: 12, + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center', + }, + buttonText: { + color: '#fff', + fontWeight: '700', + fontSize: 14, + }, + loadingOverlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0,0,0,0.2)', + justifyContent: 'center', + alignItems: 'center', + zIndex: 10, + }, +}); diff --git a/src/Screens/Tender/PurchaseTenderAllList.tsx b/src/Screens/Tender/PurchaseTenderAllList.tsx new file mode 100644 index 0000000..59d1e0d --- /dev/null +++ b/src/Screens/Tender/PurchaseTenderAllList.tsx @@ -0,0 +1,152 @@ +import React, { useState, useCallback, memo, useEffect } from 'react'; +import { View, Text, StyleSheet, FlatList, RefreshControl, ActivityIndicator } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import moment from 'moment'; +import { useFetchPurchaseTender } from '../../Services/BBPS/Hooks'; +import PurchaseTenderTile from './PurchaseTenderTile'; +import { useDispatch } from 'react-redux'; +import { setTender } from '../../Redux/Slices/SelectedTender'; +import { SearchInput } from '../../Component/Serach/SeacrhInput'; +import { Skelton } from '../../Component/Index'; +import { allBooleanProps } from './types/types'; +import FilterModal from './Modal/FilterModal'; +import colors from '../../Constant/Color'; +import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated'; +import NavigationString from '../../Constant/NavigationString'; + + +const PurchaseTenderAllList = () => { + const navigation = useNavigation() as any; + const Dispatch = useDispatch() + + const [allBoolean, setAllBoolean] = useState({ isShowFilter: false }) + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch }: any = useFetchPurchaseTender(); + const [showRefreshBanner, setShowRefreshBanner] = useState(false); + + + console.log("data for tender : ", data) + useEffect(() => { + if (isRefetching) { + setShowRefreshBanner(true); + } else { + const timer = setTimeout(() => { + setShowRefreshBanner(false); + }, 1000); + + return () => clearTimeout(timer); + } + }, [isRefetching]); + const format = 'DD-MM-YYYY HH:mm:ss'; + + function isTenderActive(item: any): boolean { + const now = moment(); + const start = moment(item?.keyDates?.TenderBidding?.startDateTime, format); + const end = moment(item?.keyDates?.TenderBidding?.endDateTime, format); + + if (!start.isValid() || !end.isValid()) return false; + + return now.isBetween(start, end, undefined, '[]'); + } + + function isTenderEnd(item: any): boolean { + const now = moment(); + const end = moment(item?.keyDates?.TenderBidding?.endDateTime, format); + + if (!end.isValid()) return false; + + return now.isAfter(end); + } + const loadMore = useCallback(() => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, [hasNextPage, isFetchingNextPage]); + + + const renderFooter = useCallback(() => { + if (!isFetchingNextPage) return null; + return ( + + + + ); + }, [isFetchingNextPage]); + + return ( + + { }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} /> + + {showRefreshBanner && ( + + + + Refreshing tenders + + + )} + {isLoading ? + : String(item.tenderId || item.id)} + showsVerticalScrollIndicator={false} + contentContainerStyle={{ paddingBottom: 200, paddingHorizontal: 10 }} + renderItem={({ item, index }) => ( + { + navigation.navigate('TenderDetail', { item, onAddToPurchase: () => { }, routeFrom: 'PurchasedTender' }); + }} + onStartBidding={() => { + Dispatch(setTender(item)) + navigation.navigate(NavigationString.TenderBidding, { data: item }) + }} + buttonEnable={isTenderActive(item)} + isTenderEnd={isTenderEnd(item)} + /> + )} + onEndReached={loadMore} + onEndReachedThreshold={0.6} + ListFooterComponent={renderFooter} + refreshing={isRefetching} + onRefresh={refetch} + />} + + + {allBoolean.isShowFilter && + { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: false })) }} onApply={() => { }} /> + } + + + ); +}; + +export default PurchaseTenderAllList; + diff --git a/src/Screens/Tender/PurchaseTenderList.tsx b/src/Screens/Tender/PurchaseTenderList.tsx new file mode 100644 index 0000000..6771ab3 --- /dev/null +++ b/src/Screens/Tender/PurchaseTenderList.tsx @@ -0,0 +1,221 @@ +import React, { useState } from 'react'; +import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native'; +import Animated, { FadeIn, FadeOut, Layout, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import { ChevronDown, Trash2, Info } from 'lucide-react-native'; +import { InsideHeader } from '../../Component/Index'; +import colors from '../../Constant/Color'; +import { useNavigation } from '@react-navigation/native'; +import NavigationString from '../../Constant/NavigationString'; +import { useDispatch, useSelector } from 'react-redux'; +import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction'; + +const PurchaseTenderList = () => { + const Navigation: any = useNavigation() + const Dispatch = useDispatch() + const [expandedIndex, setExpandedIndex] = useState(null); + const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender); + const toggleExpand = (index: number) => { + setExpandedIndex(prev => (prev === index ? null : index)); + }; + + + return ( + + + + + {/* Header */} + + Tender Purchase List + + + + {/* Cards */} + {SelectedPurchaseList.map((item: any, index: any) => { + const expanded = expandedIndex === index; + return ( + + toggleExpand(index)} + > + + + + Tender No: {item.tenderNumber} + + + + Status:{' '} + + {item.tenderStatus === 'Opened' ? 'Open' : 'Approved'} + + + + + {/* Expanded Content */} + {expanded && ( + + {/* {renderRow('', item.departmentName)} */} + {/* {renderRow('Pattern', item.auctionPattern)} */} + {renderRow('Fees', `₹ ${item.documentFees}`)} + {renderRow('Department', item.departmentName)} + {renderRow('Purchase End', item.purchaseEnd)} + + { Dispatch(togglePurchaseAuction(item)) }}> + + Remove + + + )} + + ); + })} + + + {/* Footer */} + { Navigation.navigate(NavigationString.ProceedToPurTender) }}> + Proceed to Purchase + + + ); +}; + +export default PurchaseTenderList; + +const CardHeader = ({ title, expanded }: { + title: string; + expanded: boolean; +}) => { + const rotation = useSharedValue(expanded ? 1 : 0); + rotation.value = withTiming(expanded ? 1 : 0, { duration: 220 }); + const arrowStyle = useAnimatedStyle(() => ({ + transform: [{ rotate: `${rotation.value * 180}deg` }], + })); + + return ( + + {title} + + + + + ); +}; + +const renderRow = (label: string, value: string) => ( + + {label} + {value} + +); + + +const styles = StyleSheet.create({ + screen: { + flex: 1, + backgroundColor: '#F8FAFC', + }, + container: { + padding: 16, + paddingBottom: 110, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 14, + }, + heading: { + fontSize: 22, + fontWeight: '800', + color: colors.black, + }, + card: { + backgroundColor: '#fff', + borderRadius: 18, + padding: 16, + marginBottom: 14, + borderWidth: 1, + borderColor: '#E5E7EB', + }, + cardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + title: { + fontSize: 16, + fontWeight: '700', + color: '#0F172A', + flex: 1, + marginRight: 12, + }, + subTitle: { + fontSize: 14, + color: '#64748B', + marginTop: 6, + }, + details: { + marginTop: 16, + paddingTop: 14, + borderTopWidth: 1, + borderTopColor: '#E5E7EB', + }, + row: { + marginVertical: 6, + }, + label: { + fontSize: 13, + color: '#64748B', + }, + value: { + fontSize: 15, + fontWeight: '600', + color: '#0F172A', + }, + removeButton: { + marginTop: 18, + backgroundColor: colors.primary, + borderRadius: 14, + paddingVertical: 12, + flexDirection: 'row', + justifyContent: 'center', + gap: 8, + }, + removeText: { + color: '#fff', + fontWeight: '700', + }, + proceedButton: { + position: 'absolute', + bottom: 16, + left: 16, + right: 16, + backgroundColor: colors.primary, + paddingVertical: 16, + borderRadius: 18, + alignItems: 'center', + }, + proceedText: { + color: '#fff', + fontSize: 16, + fontWeight: '800', + }, +}); diff --git a/src/Screens/Tender/PurchaseTenderTile.tsx b/src/Screens/Tender/PurchaseTenderTile.tsx new file mode 100644 index 0000000..30b79db --- /dev/null +++ b/src/Screens/Tender/PurchaseTenderTile.tsx @@ -0,0 +1,297 @@ +import React, { use, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Platform, + Alert, +} from 'react-native'; +import Animated, { + FadeInUp, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated'; +import moment from 'moment'; +import CountdownTimer from '../../Component/Counter/CountdownTimer'; +import colors from '../../Constant/Color'; +import { useFetchTenderById, useProceedToBid } from '../../Services/BBPS/Hooks'; +import { useNavigation } from '@react-navigation/native'; +import TenderFullDetail from './Modal/TenderFullDetail'; + +const PurchaseTenderTile = ({ + item, + onPressDetails, +}: any) => { + const scale = useSharedValue(1); + const elevation = useSharedValue(8); + const navigation = useNavigation(); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + elevation: elevation.value, + })); + + const { data: tenderResponse, }: any = useFetchTenderById(item?.tenderId); + const tender = tenderResponse?.data; + + // console.log("item my : ", item) + + { + if (item?.nitNumber == 'TN/111') { + console.log("tender item : ", item?.nitNumber) + + } + } + + const handlePressIn = () => { + scale.value = withTiming(0.97, { duration: 120 }); + elevation.value = withTiming(3, { duration: 120 }); + }; + + const handlePressOut = () => { + scale.value = withTiming(1, { duration: 160 }); + elevation.value = withTiming(8, { duration: 160 }); + }; + + const isLive = item?.biddingStatus === true; + + const statusColor = isLive ? colors.primary : '#94A3B8'; + const lightStatusBg = isLive ? '#EFF6FF' : '#F8FAFC'; + const [showDetail, setShowDetail] = React.useState(false); + + useEffect(() => { + if(item?.tenderNumber == 'TN/210'){ + console.log("tender item : ", item) + } + }, [item]); + + return ( + + + + + + {/* Header */} + + + + + + {isLive ? 'LIVE' : 'CLOSED'} + + + {item?.tenderNumber} + + + + + ₹{Number(item?.tenderFeeInFig || 0).toLocaleString('en-IN')} + + Tender Fee + + + + {/* Date */} + { + setShowDetail(true) + }}> + + Bid Ends: + {moment( + item?.keydates?.bidSubmissionDate?.endDateTime, + 'DD-MM-YYYY HH:mm:ss' + ).format('DD MMM YYYY, hh:mm A')} + + + + {/* Footer */} + + + Remaining Time + + {isLive ? ( + + ) : ( + --:--:-- + )} + + + { + if (isLive) { + navigation.navigate('ProceedToBidDetail', { tender }); + } + }} + activeOpacity={0.85} + style={[ + styles.actionButton, + { backgroundColor: statusColor }, + !isLive && styles.disabledButton, + ]} + > + + {isLive ? 'Proceed to Bid' : 'Tender Closed'} + + + + + {/* Tender Detail Modal */} + + setShowDetail(false)} /> + + + + ); +}; + +export default PurchaseTenderTile; + + +const styles = StyleSheet.create({ + shadowContainer: { + marginHorizontal: 5, + marginTop: 14, + marginBottom: 6, + borderRadius: 16, + backgroundColor: '#fff', + elevation: 8, + }, + cardInternal: { + borderRadius: 16, + overflow: 'hidden', + flexDirection: 'row', + backgroundColor: '#fff', + }, + statusAccent: { + width: 5, + height: '100%', + }, + mainInfo: { + flex: 1, + padding: 16, + }, + headerRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: 12, + }, + statusBadge: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 8, + paddingVertical: 3, + borderRadius: 6, + marginBottom: 6, + alignSelf: 'flex-start', + }, + statusDot: { + width: 6, + height: 6, + borderRadius: 3, + marginRight: 5, + }, + statusText: { + fontSize: 10, + fontWeight: '800', + letterSpacing: 0.5, + }, + title: { + fontWeight: '800', + fontSize: 16, + color: '#1E293B', + }, + amountContainer: { + alignItems: 'flex-end', + }, + amount: { + fontWeight: '900', + fontSize: 18, + color: '#0F172A', + }, + feeLabel: { + fontSize: 10, + color: '#64748B', + fontWeight: '500', + }, + dateRow: { + backgroundColor: '#F8FAFC', + paddingHorizontal: 10, + paddingVertical: 8, + borderRadius: 10, + marginBottom: 16, + borderWidth: 1, + borderColor: '#F1F5F9', + }, + dateText: { + fontSize: 12, + color: '#334155', + fontWeight: '600', + }, + dateLabel: { + fontWeight: '400', + color: '#64748B', + }, + footer: { + borderTopWidth: 1, + borderTopColor: '#F1F5F9', + paddingTop: 12, + }, + footerTop: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + }, + footerLabel: { + fontSize: 11, + fontWeight: '600', + color: '#94A3B8', + textTransform: 'uppercase', + }, + endedText: { + fontSize: 13, + fontWeight: '700', + color: '#CBD5E1', + }, + actionButton: { + width: '100%', + height: 48, + borderRadius: 12, + justifyContent: 'center', + alignItems: 'center', + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + }, + }), + }, + disabledButton: { + backgroundColor: '#E2E8F0', + }, + addText: { + color: '#fff', + fontSize: 14, + fontWeight: '800', + letterSpacing: 0.2, + }, +}); diff --git a/src/Screens/Tender/Tender.tsx b/src/Screens/Tender/Tender.tsx index f2d6c67..b293127 100644 --- a/src/Screens/Tender/Tender.tsx +++ b/src/Screens/Tender/Tender.tsx @@ -1,12 +1,162 @@ -import { View, Text } from 'react-native' -import React from 'react' +import React, { useState, useRef } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Animated, + Dimensions, +} from 'react-native'; +import { InsideHeader } from '../../Component/Index'; +import { + useFetchLiveTender, + useFetchPurchaseTender, +} from '../../Services/BBPS/Hooks'; +import colors from '../../Constant/Color'; +import { ShoppingCart } from 'lucide-react-native'; +import { useNavigation } from '@react-navigation/native'; +import NavigationString from '../../Constant/NavigationString'; +import FontsFamily from '../../Constant/FontsFamily'; +import { useSelector } from 'react-redux'; +import LiveTender from './LiveTender'; +import PurchaseTenderAllList from './PurchaseTenderAllList'; +const { width } = Dimensions.get('screen') +const TAB_WIDTH = 140; +const TAB_HEIGHT = 44; const Tender = () => { - return ( - - Tender - - ) -} + const Navigation = useNavigation() as any + const { data: liveTenderData = {} }: any = useFetchLiveTender(); + const { data: purchaseTenderData = {} } : any = useFetchPurchaseTender(); + const SelectedPurchaseList = useSelector((state: any) => state?.selectPurchaseTender); -export default Tender \ No newline at end of file + const liveCount = liveTenderData?.totalCount || 0; + + const purchaseCount = purchaseTenderData?.pages?.[0]?.totalCount || 0; + const [selectedTab, setSelectedTab] = useState<'live' | 'purchase'>('live'); + + const translateX = useRef(new Animated.Value(0)).current; + + const onTabPress = (tab: 'live' | 'purchase', index: number) => { + setSelectedTab(tab); + Animated.spring(translateX, { + toValue: index * TAB_WIDTH, + useNativeDriver: true, + friction: 8, + }).start(); + }; + + return ( + + + + {/* Switch Tabs */} + + + + + onTabPress('live', 0)} + > + + Live ({liveCount}) + + + + onTabPress('purchase', 1)} + > + + Purchased ({purchaseCount}) + + + + {/* Cart */} + + {SelectedPurchaseList.length && + {SelectedPurchaseList.length} + } + { Navigation.navigate(NavigationString.PurchaseTenderList) }}> + + + + + {selectedTab === 'live' ? : } + + ); +}; + +export default Tender; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.white, + }, + + /* ---------- Switch UI ---------- */ + + switchWrapper: { + flexDirection: 'row', + marginVertical: 14, + backgroundColor: '#f1f5f9', + borderRadius: 18, + padding: 2, + height: TAB_HEIGHT, + }, + + tab: { + width: TAB_WIDTH, + height: TAB_HEIGHT - 4, + alignItems: 'center', + justifyContent: 'center', + zIndex: 2, + }, + + tabText: { + fontSize: 14, + color: '#64748b', + lineHeight: 18, + fontFamily: FontsFamily.poppinsSemiBold + }, + + activeText: { + color: colors.primary, + }, + + activeTab: { + position: 'absolute', + top: 2, + left: 2, + width: TAB_WIDTH, + height: TAB_HEIGHT - 4, + backgroundColor: '#fff', + borderRadius: 16, + shadowColor: '#000', + shadowOpacity: 0.1, + shadowRadius: 6, + elevation: 3, + }, +}); diff --git a/src/Screens/Tender/tenderBidding/TenderBidding.tsx b/src/Screens/Tender/tenderBidding/TenderBidding.tsx new file mode 100644 index 0000000..53b7ec3 --- /dev/null +++ b/src/Screens/Tender/tenderBidding/TenderBidding.tsx @@ -0,0 +1,664 @@ +import { + ActivityIndicator, + Alert, + FlatList, + ScrollView, + StyleSheet, + Text, + ToastAndroid, + TouchableOpacity, + View, +} from 'react-native'; +import React, { use, useEffect, useState } from 'react'; +import { useNavigation, useRoute } from '@react-navigation/native'; +import MyInput from '../../../../src/Component/MyInput'; +import { pick, types } from '@react-native-documents/picker'; +import { InsideHeader } from '../../../Component/Index'; +import colors from '../../../Constant/Color'; + +type EnvelopeType = 'A' | 'B' | 'C'; +type EnvelopeFormState = { + A: Record; + B: Record; + C: Record; +}; + +interface EnvelopeButtonProps { + title: string; + onPress: () => void; +} + +interface InfoRowProps { + title: string; + value: string; +} + +interface TenderBiddingData { + data?: { + tenderNumber?: string; + nitNo?: string; + emdInFig?: string; + tenderRebid?: boolean; + showEA?: boolean; + showEB?: boolean; + showEC?: boolean; + tenderEnvelopeAList?: any[]; + tenderEnvelopeBList?: any[]; + tenderEnvelopeCList?: any[]; + }; +} + +const TenderBidding = () => { + const route = useRoute(); + const navigation = useNavigation(); + + const [bidData] = useState(route.params?.tenderData || null); + const tenderData = route.params?.bidData || null; + const [activeEnvelope, setActiveEnvelope] = useState('A'); + const [isLoading, setIsLoading] = useState(false); + const [envelopForm, setEnvelopForm] = useState({ + A: {}, + B: {}, + C: {}, + }); + const [loading, setLoading] = useState(false) + const [loadingUpload, setLoadingUpload] = useState(false) + const [check, setCheck] = useState(false) + + + const InfoRow = ({ title, value }: InfoRowProps) => ( + + {title} + {value || '-'} + + ); + + const EnvelopeButton = ({ title, envelopeType, onPress }: EnvelopeButtonProps & { envelopeType: EnvelopeType }) => ( + + {title} + + ); + + + const handleEnvelopeChange = ( + envelopeType: EnvelopeType, + sequenceId: number, + value: any + ) => { + setEnvelopForm(prev => ({ + ...prev, + [envelopeType]: { + ...prev[envelopeType], + [sequenceId]: value, + }, + })); + }; + + const pickPdf = async ( + envelopeType: EnvelopeType, + sequenceId: number + ) => { + try { + const res = await pick({ + type: types.pdf, + allowMultiSelection: false, + }); + + const file = res[0]; + + handleEnvelopeChange(envelopeType, sequenceId, file); + } catch (err: any) { + if (err?.code === 'DOCUMENT_PICKER_CANCELED') { + return; + } + console.error(err); + } + }; + + + const renderEnvelopeItem = ({ + item, + envelopeType, + }: { + item: any; + envelopeType: EnvelopeType; + }) => { + const sequenceId = item?.[`tenderEnvelope${envelopeType}SequnceId`]; + + if (!sequenceId) { + return null; + } + + const labelName = item?.[`labelName${envelopeType}`] || ''; + const labelType = item?.[`labelType${envelopeType}`] || ''; + + const showInput = + activeEnvelope === envelopeType !== 'C' && + !bidData?.[`envelope${envelopeType}SubmittStatus`]; + + + return ( + + + + {/* Label Name */} + {labelName || '-'} + {item?.[`labelMandatory${activeEnvelope}`] && *} + + + + {/* + Label Type + {labelType || '-'} + */} + + + {showInput ? ( + <> + {labelType === '1' && ( + + handleEnvelopeChange(envelopeType, sequenceId, text) + } + keyboardType="numeric" + /> + )} + + {labelType === '2' && ( + + handleEnvelopeChange(envelopeType, sequenceId, text) + } + keyboardType="default" + /> + )} + {labelType === '3' && ( + <> + pickPdf(activeEnvelope, sequenceId)} + > + + {envelopForm?.[activeEnvelope]?.[sequenceId] + ? 'Change Document' + : 'Upload Document'} + + + + {envelopForm?.[activeEnvelope]?.[sequenceId] && ( + + {(envelopForm[activeEnvelope][sequenceId] as any)?.name} + + )} + + + )} + + {labelType === '4' && ( + + handleEnvelopeChange(envelopeType, sequenceId, text) + } + keyboardType="numeric" + /> + )} + + + ) : Envelop {activeEnvelope} unavailable} + + ); + }; + + const hasEnvelopes = () => { + const hasA = tenderData?.tenderEnvelopeAList?.length > 0; + const hasB = tenderData?.tenderEnvelopeBList?.length > 0; + // const hasC = tenderData?.tenderEnvelopeCList?.length > 0; + return hasA || hasB; + }; + + const digitToWordMap: Record = { + '0': 'Zero', + '1': 'One', + '2': 'Two', + '3': 'Three', + '4': 'Four', + '5': 'Five', + '6': 'Six', + '7': 'Seven', + '8': 'Eight', + '9': 'Nine', + }; + + const convertDigitsToWords = (value: string): string => { + return value + .split('') + .map(digit => digitToWordMap[digit] || '') + .join(' ') || value; + }; + + const buildEnvelopePayload = () => { + const list = + bidData?.[`tenderEnvelope${activeEnvelope}List`] || []; + const formValues = envelopForm?.[activeEnvelope] || {}; + + // 🔹 NON-FILE FIELDS (labelType !== '3') + const saveData = list + .filter( + (item: any) => + item[`labelType${activeEnvelope}`] !== '3' + ) + .map((item: any) => { + const seqId = + item[`tenderEnvelope${activeEnvelope}SequnceId`]; + const value = formValues[seqId]; + + return { + [`tenderEnvelope${activeEnvelope}SequnceId`]: seqId, + [`labelName${activeEnvelope}`]: + item[`labelName${activeEnvelope}`], + [`labelType${activeEnvelope}`]: + item[`labelType${activeEnvelope}`], + [`labelMandatory${activeEnvelope}`]: + item[`labelMandatory${activeEnvelope}`], + fieldValue: value ?? '', + fieldValueInWord: value + ? convertDigitsToWords(value) + : '', + }; + }); + + // 🔹 FILE FIELDS (labelType === '3') → upload separately + const fileData = list + .filter( + (item: any) => + item[`labelType${activeEnvelope}`] === '3' + ) + .map((item: any) => { + const seqId = + item[`tenderEnvelope${activeEnvelope}SequnceId`]; + + return { + [`tenderEnvelope${activeEnvelope}SequnceId`]: seqId, + [`labelName${activeEnvelope}`]: + item[`labelName${activeEnvelope}`], + [`labelType${activeEnvelope}`]: + item[`labelType${activeEnvelope}`], + [`labelMandatory${activeEnvelope}`]: + item[`labelMandatory${activeEnvelope}`], + }; + }); + + return payload = { + deptId: bidData?.deptId, + tenderId: bidData?.tenderId, + + + [`tenderEnvelope${activeEnvelope}NameId`]: + bidData?.[`tenderEnvelope${activeEnvelope}List`]?.[0]?.[ + `tenderEnvelope${activeEnvelope}NameId` + ], + [`envelope${activeEnvelope}TemplateName`]: + bidData?.[`tenderEnvelope${activeEnvelope}List`]?.[0]?.[ + `envelope${activeEnvelope}TemplateName` + ], + + [`save${activeEnvelope}Data`]: envelopForm?.[activeEnvelope]?.[3] + ? fileData + : saveData, + }; + + }; + + + const validateEnvelope = () => { + const list = bidData?.[`tenderEnvelope${activeEnvelope}List`] || []; + const formValues = envelopForm[activeEnvelope]; + + const missing = list.some((item: any) => { + const seqId = item[`tenderEnvelope${activeEnvelope}SequnceId`]; + const isMandatory = item[`labelMandatory${activeEnvelope}`]; + const value = formValues?.[seqId]; + + if (!isMandatory) return false; + + // File upload + if (item[`labelType${activeEnvelope}`] === '3') { + return !value; + } + + // Text / numeric + return !value || value.toString().trim() === ''; + }); + + return !missing; + }; + + + const handleSubmitEnvelopeA = async () => { + if (!validateEnvelope()) { + Alert.alert('Validation Error', 'Please fill all mandatory fields'); + return; + } + + const payload = buildEnvelopePayload(); + console.log('Envelope A Payload:', payload); + setLoading(true) + if (envelopForm?.[activeEnvelope]?.[3]) { + handleSubmitEnvelopeFile() + } + try { + + const res = await apiCallForPostWithToken(SUBMIT_ENVELOPE_A, payload) + console.log("res A envelop : ", res) + ToastAndroid.show(res.message, ToastAndroid.SHORT) + + + navigation.goBack() + } catch (error) { + console.log("error A envelop submit : ", error) + } finally { + setLoading(false) + } + }; + + + const handleSubmitEnvelopeB = async () => { + if (!validateEnvelope()) { + Alert.alert('Validation Error', 'Please fill all mandatory fields'); + return; + } + setLoading(true) + const payload = buildEnvelopePayload(); + console.log('Envelope B Payload:', payload); + if (envelopForm?.[activeEnvelope]?.[3]) { + handleSubmitEnvelopeFile() + } + try { + const res = await apiCallForPostWithToken(SUBMIT_ENVELOPE_B, payload) + console.log("res B envelop : ", res) + + ToastAndroid.show(res.message, ToastAndroid.SHORT) + navigation.goBack() + } catch (error) { + console.log("error B envelop submit : ", error) + } finally { + setLoading(false) + } + } + + const handleSubmitEnvelopeFile = async () => { + + setLoadingUpload(true) + const payload = buildEnvelopePayload(); + console.log(`Envelope file ${activeEnvelope} Payload:`, payload); + try { + const formData = new FormData(); + + formData.append('files', { + uri: envelopForm?.[activeEnvelope]?.[3].uri, + name: envelopForm?.[activeEnvelope]?.[3].name, + type: envelopForm?.[activeEnvelope]?.[3].type, + }); + formData.append('requestDto', payload) + + + console.log("reuestDto for fil doc : ", formData) + // return + const res = await apiCallForPostWithToken(activeEnvelope === 'B' ? SUBMIT_ENVELOPE_B : SUBMIT_ENVELOPE_A, payload) + console.log(`res file ${activeEnvelope} Payload:`, res) + ToastAndroid.show(res.message, ToastAndroid.SHORT) + // navigation.goBack() + } catch (error) { + console.log(`Envelope file ${activeEnvelope} error:`, error) + } finally { + setLoadingUpload(false) + } + } + + if (isLoading) { + return ( + <> + + Loading... + + + ); + } + + // console.log("route for date : ", bidData) + // console.log("bid envelop : ", tenderData) + return ( + + + + {/* Tender Info */} + + + + + + + + + + + + + {/* Envelope Buttons */} + + Bid Envelopes + + {tenderData?.showEA && ( + setActiveEnvelope('A')} + /> + )} + {tenderData?.showEB && ( + { + if (tenderData?.envelopeASubmittStatus) { + setActiveEnvelope('B') + } else { + Alert.alert('Info', 'First Fill Envelop A First') + } + }} + /> + )} + {tenderData?.showEC && ( + setActiveEnvelope('C')} + /> + )} + + + {/* Envelope A List */} + {tenderData?.[`tenderEnvelope${activeEnvelope}List`]?.length > 0 && activeEnvelope !== 'C' && ( + + Envelope {activeEnvelope} Details + item?.[`tenderEnvelope${activeEnvelope}SequnceId`]?.toString()} + renderItem={({ item }) => renderEnvelopeItem({ item, envelopeType: activeEnvelope })} + scrollEnabled={false} + /> + {/* + setCheck(!check)}> + {check ? : + + } + Check First Terms&Condition + */} + + )} + + {hasEnvelopes() ? ( + { + if (!check) { + Alert.alert('Info', 'check first terms & condition') + } + if (activeEnvelope === 'A') { + handleSubmitEnvelopeA() + } else { + handleSubmitEnvelopeB() + } + }} + > + {loading ? 'Submitting...' : `Submit Bid (${activeEnvelope})`} + + ) : ( + navigation.goBack()} + + > + Go Back + + )} + + + + + ); +}; + +export default TenderBidding; + +const styles = StyleSheet.create({ + container: { + flex: 1, backgroundColor: colors.white + }, + scrollContainer: { + padding: 16, + paddingBottom: '100%', + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + card: { + backgroundColor: '#fff', + borderRadius: 12, + padding: 16, + marginBottom: 16, + elevation: 2, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + }, + sectionTitle: { + fontSize: 16, + fontWeight: '600', + marginBottom: 12, + color: '#333', + }, + row: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 8, + }, + infoRow: { + flex: 1, + marginBottom: 10, + }, + label: { + fontSize: 13, + color: '#777', + marginBottom: 2, + }, + value: { + fontSize: 15, + color: '#222', + fontWeight: '500', + }, + envelopeRow: { + flexDirection: 'row', + gap: 10, + flexWrap: 'wrap', + }, + envelopeButton: { + backgroundColor: '#EEF2FF', + paddingVertical: 10, + paddingHorizontal: 14, + borderRadius: 8, + minWidth: 100, + alignItems: 'center', + }, + envelopeButtonText: { + color: '#3F51B5', + fontWeight: '600', + fontSize: 14, + }, + envelopeItem: { + borderWidth: 1, + borderColor: '#eee', + borderRadius: 10, + padding: 12, + marginBottom: 12, + backgroundColor: '#f9f9f9', + flexDirection: 'row', + justifyContent: 'space-between' + }, + footer: { + flexDirection: 'row', + justifyContent: 'flex-end', + marginVertical: 20, + }, + primaryButton: { + backgroundColor: '#3F51B5', + paddingVertical: 14, + paddingHorizontal: 28, + borderRadius: 10, + minWidth: 120, + alignItems: 'center', + }, + primaryButtonText: { + color: '#fff', + fontWeight: '600', + fontSize: 15, + }, + secondaryButton: { + borderWidth: 1, + borderColor: '#EEF2FF', + paddingVertical: 14, + paddingHorizontal: 28, + borderRadius: 10, + minWidth: 120, + alignItems: 'center', + }, + secondaryButtonText: { + color: '#3F51B5', + fontWeight: '600', + fontSize: 15, + }, +}); \ No newline at end of file diff --git a/src/Screens/Tender/types/types.tsx b/src/Screens/Tender/types/types.tsx new file mode 100644 index 0000000..5ec8e48 --- /dev/null +++ b/src/Screens/Tender/types/types.tsx @@ -0,0 +1,18 @@ +export interface LiveTenderTileProps { + data: any + onPurchasePress?: () => void; + onDetailsPress?: () => void; + index: number; + tenderDispatchDate?: string; + tenderOfWork?: string; + purchaseEnd?: string; + tenderFees?: string | number; + isPurchased?: boolean; + tenderId?: any; + tenderNumber?: any; +} + +export interface allBooleanProps { + showDetail?: boolean + isShowFilter?: boolean +} \ No newline at end of file diff --git a/src/Screens/index.ts b/src/Screens/index.ts index e5e7037..0133591 100644 --- a/src/Screens/index.ts +++ b/src/Screens/index.ts @@ -8,7 +8,12 @@ import Home from "./Home/Home"; import Login from "./Login/Login"; import Onboarding from "./Onboarding/Onboarding"; import Splash from "./Splash/Splash"; +import LiveTenderDetail from "./Tender/LiveTenderDetail"; +import ProceedToBidDetail from "./Tender/ProceedToBidDetail"; +import ProceedToPurTender from "./Tender/ProceedToPurTender"; +import PurchaseTenderList from "./Tender/PurchaseTenderList"; import Tender from "./Tender/Tender"; +import TenderBidding from "./Tender/tenderBidding/TenderBidding"; const Screens = { @@ -22,6 +27,11 @@ const Screens = { Emd, StartBidding, PurchaseAuctionList, - StartReverseBidding + StartReverseBidding, + PurchaseTenderList, + ProceedToPurTender, + TenderBidding, + ProceedToBidDetail, + LiveTenderDetail } export default Screens \ No newline at end of file diff --git a/src/Services/BBPS/ApiUrls.tsx b/src/Services/BBPS/ApiUrls.tsx index 270a659..bb6adc2 100644 --- a/src/Services/BBPS/ApiUrls.tsx +++ b/src/Services/BBPS/ApiUrls.tsx @@ -32,8 +32,9 @@ const PROCEED_PURCHASE_AUCTION = `${BASE_URL_Auction}api/auction/proceedToPurcha const PURCHASE_AUCTION = `${BASE_URL_Auction}api/auction/purchaseAuction` const PURCHASE_OFFLINE_AUCTION = `${BASE_URL_Auction}api/auction/purchaseAuctionOffline` const PURCHASE_TENDER = `https://procurelinc.in/EProcurementSB/tenderPayment/purchaseTender` +const PURCHASE_TENDER_OFFLINE = `https://procurelinc.in/EProcurementSB/tenderPayment/purchaseTenderOffline` const AUCTION_PURCHASE_STATUS = `https://api.godemo.in/Auction/api/auction/transactionStatus?orderNo=` -const GET_TENDER_DETAILS = `${BASE_URL}eTendering/getTenderById?tenderId=` +const GET_TENDER_DETAILS = `https://procurelinc.in/EProcurementSB/tenderBidding/getTenderDetail?tenderId=` const GET_DOCID = `${BASE_URL}eTendering/download/docId?docId=` // https://procurelinc.in/EProcurementSB/eTendering/download/docId?docId=158 // const DOWNLOAD_DOC = `${BASE_URL}eTende ring/download/docId?docId=` @@ -69,6 +70,17 @@ const VIEW_AUCTION_DOCUMENTS = `${BASE_URL_Auction}api/auction/viewDocumentsForA const AUCTION_DOCUMENT_DOWNLOAD = `${BASE_URL_Auction}api/auction/home/downloadDocumentForAuction` const AUCTION_ITEM_DOCUMENT_DOWNLOAD = `${BASE_URL_Auction}api/auction/home/downloadDocument/auctionItemId` +//tender + +const TENDER_LOGIN = `${BASE_URL}eproc/loginWithoutDecryption` +const PROCEED_TO_BID = `${BASE_URL}tenderBidding/proceedToBid?tenderId=` +const SUBMIT_ENVELOPE_A = `${BASE_URL}tenderBidding/getSaveEnvelopeAData` +const SUBMIT_ENVELOPE_B = `${BASE_URL}tenderBidding/getSaveEnvelopeBData` +const SUBMIT_ENVELOPE_FILE_A = `${BASE_URL}tenderBidding/addDocumentsEnvelopeA?requestDto=` +const SUBMIT_ENVELOPE_FILE_B = `${BASE_URL}tenderBidding/addDocumentsEnvelopeB?requestDto=` + + + export { // Rohit VIEW_AUCTION_ITEM_DETAIL, @@ -132,6 +144,13 @@ export { AUCTION_LOGIN, TWO_FACTOR_AUTH, GET_MASTER_DATA, - PURCHASE_OFFLINE_AUCTION + PURCHASE_OFFLINE_AUCTION, + TENDER_LOGIN, + PURCHASE_TENDER_OFFLINE, + PROCEED_TO_BID, + SUBMIT_ENVELOPE_A, + SUBMIT_ENVELOPE_B, + SUBMIT_ENVELOPE_FILE_A, + SUBMIT_ENVELOPE_FILE_B }; diff --git a/src/Services/BBPS/Hooks.ts b/src/Services/BBPS/Hooks.ts index a362d0a..94ea64c 100644 --- a/src/Services/BBPS/Hooks.ts +++ b/src/Services/BBPS/Hooks.ts @@ -1,5 +1,5 @@ import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'; -import { fetchAuctionItem, fetchDashboard, fetchEmdCheck, fetchLiveAuction, fetchLiveTender, fetchPurchaseAuction, fetchPurchaseAuctionList, fetchPurchaseTender, getAuction, offlineAuctionPurchase, userLogin, viewAuctionDocumentDownload, viewAuctionDocuments, viewAuctionItemDocuments } from './apis'; +import { fetchAuctionItem, fetchDashboard, fetchEmdCheck, fetchLiveAuction, fetchLiveTender, fetchPurchaseAuction, fetchPurchaseAuctionList, fetchPurchaseTender, getAuction, offlineAuctionPurchase, userLogin, offlineTenderPurchase, fetchTenderById, proceedToBid, viewAuctionDocumentDownload, viewAuctionDocuments, viewAuctionItemDocuments } from './apis'; // User @@ -20,15 +20,38 @@ export const useDashboard = () => { // Auction & Tender export const useFetchPurchaseTender = () => { - return useQuery({ - queryKey: [], - queryFn: () => fetchPurchaseTender() + return useInfiniteQuery({ + queryKey: ['fetchPurchaseTender'], + queryFn: ({ pageParam = 0 }) => + fetchPurchaseTender({ + page: pageParam, + size: 10, + }), + initialPageParam: 0, + getNextPageParam: (lastPage, allPages) => { + console.log('last page : ', lastPage) + console.log("all page : ", allPages) + const {totalCount } = lastPage; + + const totalPages = Math.ceil(totalCount / 10); + const nextPage = allPages.length; + return nextPage < totalPages ? nextPage : undefined; + }, + + select: (data) => ({ + ...data, + result: data.pages.flatMap( + (page: any) => page?.data ?? [] + ), + }), }); }; + + export const useFetchLiveTender = () => { return useQuery({ - queryKey: [], + queryKey: ['fetchLiveTender',], queryFn: () => fetchLiveTender() }); }; @@ -78,6 +101,30 @@ export const useOfflineAuctionPurchase = () => { mutationFn: (payload: any) => offlineAuctionPurchase(payload), }); }; +export const useOfflineTenderPurchase = () => { + return useMutation({ + mutationFn: (payload: any) => offlineTenderPurchase(payload), + }); +}; + +export const useFetchTenderById = (tenderId: string) => { + return useQuery({ + queryKey: ['fetchTenderById', tenderId], + queryFn: () => fetchTenderById(tenderId) + + }); +} + +export const useProceedToBid = (tenderId?: string) => { + return useQuery({ + queryKey: ['proceedToBid', tenderId], + queryFn: () => proceedToBid(tenderId as string), + enabled: false, // 👈 manual trigger + }); +}; + + + // EMD export const useEmdCheck = () => { diff --git a/src/Services/BBPS/apis.ts b/src/Services/BBPS/apis.ts index ea7f34b..c82c2f8 100644 --- a/src/Services/BBPS/apis.ts +++ b/src/Services/BBPS/apis.ts @@ -1,12 +1,13 @@ import axios from "axios"; -import { AUCTION_DOCUMENT_DOWNLOAD, DASHBOARD_COUNT, EMD_CHECK, FORARD_AUCTION_ITEM, GET_AUCTION_DETAILS, GET_MY_AUCTION, GET_MY_TENDER, LIVE_AUCTION, LIVE_TENDER, LOG_IN_URL, PROCEED_PURCHASE_AUCTION, PURCHASE_AUCTION_LIST, PURCHASE_OFFLINE_AUCTION, REVERSE_AUCTION_ITEM, VIEW_AUCTION_DOCUMENTS, VIEW_AUCTION_ITEM_DETAIL } from "./ApiUrls"; +import { AUCTION_DOCUMENT_DOWNLOAD, DASHBOARD_COUNT, EMD_CHECK, FORARD_AUCTION_ITEM, GET_AUCTION_DETAILS, GET_MY_AUCTION, GET_MY_TENDER, GET_TENDER_DETAILS, LIVE_AUCTION, LIVE_TENDER, LOG_IN_URL, PROCEED_PURCHASE_AUCTION, PROCEED_TO_BID, PURCHASE_TENDER_OFFLINE, TENDER_LOGIN, PURCHASE_AUCTION_LIST, PURCHASE_OFFLINE_AUCTION, REVERSE_AUCTION_ITEM, VIEW_AUCTION_DOCUMENTS, VIEW_AUCTION_ITEM_DETAIL } from "./ApiUrls"; import { apiCall } from "../../Axios/Axios"; // User export const userLogin = async (payload: { emailAdd: string, password: string }): Promise => { try { - const response = await axios.post(LOG_IN_URL, { ...payload, divice: "mobileApp" }); + // const response = await axios.post(LOG_IN_URL, { ...payload, divice: "mobileApp" }); + const response = await axios.post(TENDER_LOGIN, { ...payload, device: "mobileApp" }); return response.data; } catch (error: any) { throw error.response?.data || error.message; @@ -18,15 +19,6 @@ export const fetchDashboard = async (): Promise => { return data } - -export const fetchPurchaseTender = async (): Promise => { - const data = await apiCall('get', `${GET_MY_TENDER}`); - return data -} -export const fetchLiveTender = async (): Promise => { - const data = await apiCall('get', `${LIVE_TENDER}`); - return data -} export const fetchPurchaseAuction = async (): Promise => { const data = await apiCall('get', `${GET_MY_AUCTION}`); return data @@ -73,6 +65,53 @@ export const fetchAuctionItem = async (payload: any): Promise => { } }; +// Tender Purchase offline +export const offlineTenderPurchase = async (payload: any): Promise => { + try { + const response = await apiCall('post', PURCHASE_TENDER_OFFLINE, payload); + return response.data; + } catch (error: any) { + throw error.response?.data || error.message; + } +}; + +// tender +export const fetchPurchaseTender = async ({ + page, + size, +}: { + page: number; + size: number; +}) => { + return apiCall( + 'get', + `${GET_MY_TENDER}?page=${page}&size=${size}` + ); +}; + + + +export const fetchLiveTender = async (): Promise => { + const data = await apiCall('get', `${LIVE_TENDER}`); + return data +} +export const fetchTenderById = async (tenderId: string): Promise => { + const data = await apiCall( + 'get', + `${GET_TENDER_DETAILS}${tenderId}` + ); + return data; +}; + +export const proceedToBid = async (tenderId: string): Promise => { + const data = await apiCall( + 'get', + `${PROCEED_TO_BID}${tenderId}` + ); + return data; +} + + export const fetchPurchaseAuctionList = async (payload: any): Promise => { const data = await apiCall('post', `${PURCHASE_AUCTION_LIST}`, payload);