updated code with dsc and device binding

This commit is contained in:
Abin Toppo
2026-05-08 13:39:41 +05:30
parent c1ecc97e29
commit 66461370a2
120 changed files with 11910 additions and 8347 deletions
@@ -21,16 +21,16 @@ import { encrypt, encryptWithRSA } from '../../../Services/Encryption/encrypt';
/* -------------------- UTILS -------------------- */
interface OfficerKey {
officersId: number;
officerPublicKey: string;
officerType: string;
officersId: number;
officerPublicKey: string;
officerType: string;
}
interface SaveEnvelopeOfficerDetail {
officerId: number;
officerEncryptedKey: string;
officerType: string;
officerId: number;
officerEncryptedKey: string;
officerType: string;
}
// Convert ArrayBuffer → Base64
@@ -61,7 +61,7 @@ const isTotalRow = (row: any) =>
const EnvelopeCExcelPreview = (props: any) => {
const tenderData = props;
console.log("Tender Data",tenderData)
console.log("Tender Data", tenderData)
const token = useSelector((state: any) => state.token.token);
const navigation = useNavigation() as any;
@@ -146,8 +146,8 @@ const EnvelopeCExcelPreview = (props: any) => {
if (isTotalRow(row)) return;
const qtyKey:any = findColumnKey(columns, ['qty', 'quantity']);
const rateKey:any = findColumnKey(columns, ['rate', 'price']);
const qtyKey: any = findColumnKey(columns, ['qty', 'quantity']);
const rateKey: any = findColumnKey(columns, ['rate', 'price']);
const valueKey = findColumnKey(columns, ['value', 'amount', 'total']);
@@ -253,7 +253,7 @@ const EnvelopeCExcelPreview = (props: any) => {
// Encrypt the entire file content
console.log('🔒 Encrypting file content...');
const encryptedContent:any = encrypt(fileContent, encryptionKey);
const encryptedContent: any = encrypt(fileContent, encryptionKey);
console.log('✅ File content encrypted, Encrypted length:', encryptedContent?.length);
@@ -353,6 +353,7 @@ const EnvelopeCExcelPreview = (props: any) => {
deviceType: 'MOBILE',
fileName: 'envelopeC_encrypted.xlsx',
saveEnvelopesOfficersDetailsResDTO: officerEncryptedKeys,
randomKeyGenerated: ENCRYPTION_KEY,
};
@@ -375,7 +376,7 @@ const EnvelopeCExcelPreview = (props: any) => {
console.log('\n📝 STEP 6: Uploading encrypted file to server');
console.log('───────────────────────────────────────────────────────');
console.log('FormData prepared:', formData);
const response = await axios.post(SUBMIT_ENVELOPE_C_UPLOAD, formData, {
headers: {
+194
View File
@@ -0,0 +1,194 @@
import React, { useState } from 'react';
import { View, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { InsideHeader } from '../../Component/Index';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
import { useNavigation, useRoute } from '@react-navigation/native';
import { AlertCircle, CheckCircle } from 'lucide-react-native';
import DeviceInfo from 'react-native-device-info';
import { apiCall } from '../../Axios/Axios';
import { BASE_URL } from '../../Services/ApiCall/ApiUrls';
import Geolocation from 'react-native-geolocation-service';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { Platform } from 'react-native';
import { clampRGBA } from 'react-native-reanimated/lib/typescript/Colors';
const DeviceBinding = () => {
const { colors, spacing, typography } = useTheme();
const navigation = useNavigation<any>();
const route = useRoute<any>();
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const { message, showBindButton } = route.params || {
message: "Device is not mapped or not active. Please register and activate your device.",
showBindButton: false
};
const getLocation = (): Promise<{ latitude: string, longitude: string }> => {
return new Promise(async (resolve) => {
try {
const permission = Platform.OS === 'ios'
? PERMISSIONS.IOS.LOCATION_WHEN_IN_USE
: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION;
const result = await request(permission);
if (result === RESULTS.GRANTED) {
Geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude.toString(),
longitude: position.coords.longitude.toString(),
});
},
(error) => {
console.log("Location Error: ", error);
resolve({ latitude: "23.77", longitude: "77.34" }); // Fallback to standard
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
);
} else {
resolve({ latitude: "23.77", longitude: "77.34" }); // Fallback
}
} catch (err) {
console.log("Permission Error: ", err);
resolve({ latitude: "23.77", longitude: "77.34" }); // Fallback
}
});
};
const handleBindDevice = async () => {
setLoading(true);
try {
const deviceId = await DeviceInfo.getUniqueId();
const systemName = DeviceInfo.getSystemName();
const systemVersion = DeviceInfo.getSystemVersion();
const deviceOs = `${systemName} ${systemVersion}`;
const lastIp = await DeviceInfo.getIpAddress();
const brand = DeviceInfo.getBrand();
const model = DeviceInfo.getModel();
const deviceName = `${brand} ${model}`;
const browser = "Mobile App";
const { latitude, longitude } = await getLocation();
const location = `${longitude},${latitude}`;
const payload = {
browser,
deviceOs,
deviceId,
lastIp,
deviceName,
location,
};
const response: any = await apiCall('post', `${BASE_URL}tenderBidding/deviceBinding`, payload);
if (response?.status === 'SUCCESS' || response?.statusCode === 200) {
setSuccess(true);
// Optionally navigate back after a delay
setTimeout(() => {
navigation.goBack();
}, 2000);
}
} catch (error) {
console.log("Error binding device: ", error);
} finally {
setLoading(false);
}
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.l,
},
iconContainer: {
marginBottom: spacing.l,
},
title: {
marginBottom: spacing.m,
textAlign: 'center',
},
message: {
textAlign: 'center',
marginBottom: spacing.xl,
lineHeight: 24,
},
button: {
backgroundColor: colors.primary,
paddingVertical: spacing.m,
paddingHorizontal: spacing.xl,
borderRadius: 12,
width: '100%',
alignItems: 'center',
marginBottom: spacing.m,
},
secondaryButton: {
backgroundColor: 'transparent',
paddingVertical: spacing.m,
width: '100%',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.border,
borderRadius: 12,
}
});
return (
<View style={styles.container}>
<InsideHeader title="Device Binding" showArrow />
<View style={styles.content}>
<View style={styles.iconContainer}>
{success ? (
<CheckCircle size={64} color={colors.success} />
) : (
<AlertCircle size={64} color={colors.error} />
)}
</View>
<AppText variant="h3" color={colors.text} style={styles.title}>
{success ? "Binding Successful" : "Device Not Bound"}
</AppText>
<AppText variant="body" color={colors.textSecondary} style={styles.message}>
{success ? "Your device binding request has been submitted successfully." : message}
</AppText>
{showBindButton && !success && (
<TouchableOpacity
style={styles.button}
onPress={handleBindDevice}
disabled={loading}
>
{loading ? (
<ActivityIndicator size="small" color={colors.buttonText} />
) : (
<AppText variant="button" color={colors.buttonText}>
Bind This Device
</AppText>
)}
</TouchableOpacity>
)}
<TouchableOpacity
style={success ? styles.button : styles.secondaryButton}
onPress={() => navigation.goBack()}
>
<AppText variant="button" color={success ? colors.buttonText : colors.text}>
{success ? "Finish" : "Go Back"}
</AppText>
</TouchableOpacity>
</View>
</View>
);
};
export default DeviceBinding;
+79
View File
@@ -0,0 +1,79 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import { InsideHeader } from '../../Component/Index';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
import { useNavigation, useRoute } from '@react-navigation/native';
import { AlertCircle } from 'lucide-react-native';
const DscBinding = () => {
const { colors, spacing, typography } = useTheme();
const navigation = useNavigation<any>();
const route = useRoute<any>();
// Fallback message just in case
const message = route.params?.message || "DSC is not mapped. Please map your DSC on the web portal before proceeding.";
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.l,
},
iconContainer: {
marginBottom: spacing.l,
},
title: {
marginBottom: spacing.m,
textAlign: 'center',
},
message: {
textAlign: 'center',
marginBottom: spacing.xl,
lineHeight: 24,
},
button: {
backgroundColor: colors.primary,
paddingVertical: spacing.m,
paddingHorizontal: spacing.xl,
borderRadius: 12,
width: '100%',
alignItems: 'center',
}
});
return (
<View style={styles.container}>
<InsideHeader title="DSC Binding" showArrow />
<View style={styles.content}>
<View style={styles.iconContainer}>
<AlertCircle size={64} color={colors.error} />
</View>
<AppText variant="h3" color={colors.text} style={styles.title}>
DSC Not Bound
</AppText>
<AppText variant="body" color={colors.textSecondary} style={styles.message}>
{message}
</AppText>
<TouchableOpacity
style={styles.button}
onPress={() => navigation.goBack()}
>
<AppText variant="button" color={colors.buttonText}>
Go Back
</AppText>
</TouchableOpacity>
</View>
</View>
);
};
export default DscBinding;
+33 -11
View File
@@ -1,18 +1,19 @@
import React, { useCallback, useState, useEffect } from 'react';
import { View, FlatList, ActivityIndicator, RefreshControl, Text } from 'react-native';
import { View, FlatList, ActivityIndicator, RefreshControl, Text, StyleSheet } from 'react-native';
import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated';
import { useNavigation } from '@react-navigation/native';
import { useFetchLiveAuction, useFetchLiveTender } from '../../Services/ApiCall/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';
import { useTheme } from '../../Hooks/useTheme'; // Using Theme
const LiveTender = () => {
const navigation: any = useNavigation();
const Dispatch = useDispatch()
const { colors } = useTheme(); // Hook
const user = useSelector((state: any) => state?.token.token);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch }: any = useFetchLiveTender();
@@ -47,7 +48,7 @@ const LiveTender = () => {
const renderItem = useCallback(
({ item, index }: any) => {
if(item.tenderStatus==="Cancelled") return null;
if (item.tenderStatus === "Cancelled") return null;
const isPurchase = SelectedPurchaseList.findIndex((prev: any) => prev.tenderId === item.tenderId)
return <LiveTenderTile
contractorId={user?.id}
@@ -68,11 +69,20 @@ const LiveTender = () => {
);
if (isLoading) {
return <Skelton />;
return <View style={{ flex: 1, backgroundColor: colors.background }}>
<Skelton />
</View>;
}
return (
<View style={{ flex: 1 }}>
<SearchInput onChange={() => { }} placeholder="Search...." />
<View style={{ flex: 1, backgroundColor: colors.background }}>
{/* Liquid Background Blobs */}
<View style={styles.blobContainer}>
<View style={[styles.blob, { top: -50, right: -50, backgroundColor: colors.primary, opacity: 0.05 }]} />
<View style={[styles.blob, { top: 200, left: -100, backgroundColor: colors.secondary, opacity: 0.05 }]} />
<View style={[styles.blob, { bottom: -50, right: -20, backgroundColor: colors.primary, opacity: 0.05 }]} />
</View>
<SearchInput onChange={() => { }} placeholder="Search Tenders..." filter={false} />
{showRefreshBanner && (
<Animated.View
@@ -87,7 +97,7 @@ const LiveTender = () => {
paddingHorizontal: 14,
marginTop: 6,
borderRadius: 20,
backgroundColor: '#FFFFFF',
backgroundColor: colors.surface,
shadowColor: '#000',
shadowOpacity: 0.06,
shadowRadius: 8,
@@ -102,7 +112,7 @@ const LiveTender = () => {
<Text
style={{
fontSize: 13,
color: '#374151',
color: colors.text,
fontWeight: '500',
}}
>
@@ -124,7 +134,7 @@ const LiveTender = () => {
<RefreshControl
refreshing={isRefetching}
onRefresh={refetch}
tintColor="transparent"
tintColor={colors.primary}
colors={[colors.primary]}
/>
}
@@ -133,11 +143,23 @@ const LiveTender = () => {
windowSize={7}
removeClippedSubviews
updateCellsBatchingPeriod={50}
onRefresh={refetch}
refreshing={isRefetching}
/>
</View>
);
};
const styles = StyleSheet.create({
blobContainer: {
...StyleSheet.absoluteFillObject,
zIndex: -1,
overflow: 'hidden',
},
blob: {
position: 'absolute',
width: 300,
height: 300,
borderRadius: 150,
},
});
export default LiveTender;
+220 -196
View File
@@ -1,20 +1,15 @@
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import {
Alert,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
StatusBar,
SafeAreaView,
} from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native';
import {
useFetchLiveTenderById,
} from '../../Services/ApiCall/Hooks';
import { InsideHeader } from '../../Component/Index';
import colors from '../../Constant/Color';
import ReactNativeBlobUtil from 'react-native-blob-util';
import { GET_DOCID } from '../../Services/ApiCall/ApiUrls';
import {
@@ -27,20 +22,23 @@ import {
} from 'lucide-react-native';
import moment from 'moment';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { showSuccessAlert, showToast } from '../../Constant/ShowDailog';
import { showToast } from '../../Constant/ShowDailog';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
const InfoRow = ({ label, value }: any) => (
const InfoRow = ({ label, value, styles }: any) => (
<View style={styles.row}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.value}>{value || '-'}</Text>
<AppText variant="caption" style={styles.label}>{label}</AppText>
<AppText variant="bodySmall" style={styles.value}>{value || '-'}</AppText>
</View>
);
const Section = ({ title, icon: Icon, children }: any) => (
const Section = ({ title, icon: Icon, children, styles, colors }: any) => (
<View style={styles.sectionContainer}>
<View style={styles.sectionHeader}>
<Icon size={18} color={colors.primary} />
<Text style={styles.sectionTitle}>{title}</Text>
<AppText variant="body" style={styles.sectionTitle}>{title}</AppText>
</View>
{children}
</View>
@@ -48,11 +46,156 @@ const Section = ({ title, icon: Icon, children }: any) => (
const LiveTenderDetail = () => {
const route = useRoute<any>();
const { colors, spacing, shadows, isDarkMode } = useTheme();
const navigation = useNavigation<any>();
const tender = route?.params;
const { data } = useFetchLiveTenderById(tender?.tenderId);
const [downloadingId, setDownloadingId] = useState<string | number | null>(null);
const insets = useSafeAreaInsets()
const styles = useMemo(() => StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: colors.background,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 10,
borderBottomWidth: 1,
borderBottomColor: colors.border,
backgroundColor: colors.surface,
paddingVertical: 12,
},
backBtn: {
padding: 8,
marginRight: 8,
},
headerTitle: {
fontSize: 18,
fontWeight: '800',
color: colors.text,
},
headerSub: {
fontSize: 13,
color: colors.textSecondary,
},
liveBox: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
borderWidth: 1,
borderColor: colors.border,
backgroundColor: colors.background,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
},
liveText: {
fontSize: 11,
fontWeight: '800',
color: colors.textSecondary,
},
content: {
padding: 16,
paddingBottom: 32,
},
sectionContainer: {
backgroundColor: colors.surface,
borderRadius: 16,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: colors.border,
...shadows.light,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
marginBottom: 12,
paddingBottom: 6,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
sectionTitle: {
fontSize: 14,
fontWeight: '800',
color: colors.text,
textTransform: 'uppercase',
letterSpacing: 0.4,
},
card: {
backgroundColor: colors.surface,
borderRadius: 12,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
paddingVertical: 8,
borderBottomWidth: 0.5,
borderBottomColor: colors.border,
},
label: {
fontSize: 13,
color: colors.textSecondary,
flex: 1,
paddingRight: 8,
fontWeight: '600',
},
value: {
fontSize: 13,
color: colors.text,
flex: 1,
textAlign: 'right',
fontWeight: '500',
},
documentRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
gap: 10,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
docInfo: {
flexDirection: 'row',
alignItems: 'center',
flex: 1.4,
},
fileIcon: {
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: colors.primary + '20', // Opacity
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
},
docName: {
flex: 1,
color: colors.text,
fontSize: 13,
fontWeight: '600',
},
downloadBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
downloadText: {
fontSize: 13,
fontWeight: '700',
color: colors.primary,
},
}), [colors, shadows]);
if (!data?.data?.[0]) return null;
const tenderData = data.data[0];
@@ -77,7 +220,7 @@ const LiveTenderDetail = () => {
}).fetch('GET', downloadUrl, {
Accept: 'application/pdf',
});
const stat = await ReactNativeBlobUtil.fs.stat(path);
// const stat = await ReactNativeBlobUtil.fs.stat(path);
showToast('Download complete');
setDownloadingId(null);
} catch (error: any) {
@@ -98,193 +241,214 @@ const LiveTenderDetail = () => {
};
return (
<View style={styles.safeArea}>
<StatusBar barStyle="dark-content" />
<View style={[styles.header,{marginTop:insets.top}]}>
<View style={[styles.safeArea, { backgroundColor: colors.background }]}>
<View style={[styles.header, { marginTop: insets.top }]}>
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backBtn}
>
<ArrowLeft size={24} color="#1e293b" />
<ArrowLeft size={24} color={colors.text} />
</TouchableOpacity>
<View style={{ flex: 1 }}>
<Text style={styles.headerTitle}>Tender Overview</Text>
<Text style={styles.headerSub}>
<AppText variant="body" style={styles.headerTitle}>Tender Overview</AppText>
<AppText variant="caption" style={styles.headerSub}>
Ref: {tenderData?.tenderNumber || '-'}
</Text>
</AppText>
</View>
<View style={styles.liveBox}>
<View
style={[
styles.dot,
{ backgroundColor: isLive ? '#22c55e' : '#94a3b8' },
{ backgroundColor: isLive ? colors.success : colors.textSecondary },
]}
/>
<Text style={styles.liveText}>{isLive ? 'LIVE' : 'CLOSED'}</Text>
<AppText variant="bodySmall" style={styles.liveText}>{isLive ? 'LIVE' : 'CLOSED'}</AppText>
</View>
</View>
<ScrollView contentContainerStyle={styles.content}>
<Section title="Basic Details" icon={Layers}>
<Section title="Basic Details" icon={Layers} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow label="Tender ID" value={tenderData?.tenderId} />
<InfoRow label="Tender Number" value={tenderData?.tenderNumber} />
<InfoRow label="NIT Number" value={tenderData?.nitNumber} />
<InfoRow label="NIT Date" value={tenderData?.nitDate} />
<InfoRow label="Name of Work" value={tenderData?.nameOfWork} />
<InfoRow label="Description" value={tenderData?.descriptionOfWork} />
<InfoRow label="Tender Stage" value={tenderData?.tenderStage} />
<InfoRow label="Tender Status" value={tenderData?.tenderStatus} />
<InfoRow label="Tender ID" value={tenderData?.tenderId} styles={styles} />
<InfoRow label="Tender Number" value={tenderData?.tenderNumber} styles={styles} />
<InfoRow label="NIT Number" value={tenderData?.nitNumber} styles={styles} />
<InfoRow label="NIT Date" value={tenderData?.nitDate} styles={styles} />
<InfoRow label="Name of Work" value={tenderData?.nameOfWork} styles={styles} />
<InfoRow label="Description" value={tenderData?.descriptionOfWork} styles={styles} />
<InfoRow label="Tender Stage" value={tenderData?.tenderStage} styles={styles} />
<InfoRow label="Tender Status" value={tenderData?.tenderStatus} styles={styles} />
</View>
</Section>
<Section title="Category & Type" icon={Briefcase}>
<Section title="Category & Type" icon={Briefcase} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow label="Tender Category" value={tenderData?.tenderCategory} />
<InfoRow label="Type of Tender" value={tenderData?.typeOfTender} />
<InfoRow label="Selective Tender" value={tenderData?.selectiveTender} />
<InfoRow label="Area of Work" value={tenderData?.areaOfWork} />
<InfoRow label="Tender Category" value={tenderData?.tenderCategory} styles={styles} />
<InfoRow label="Type of Tender" value={tenderData?.typeOfTender} styles={styles} />
<InfoRow label="Selective Tender" value={tenderData?.selectiveTender} styles={styles} />
<InfoRow label="Area of Work" value={tenderData?.areaOfWork} styles={styles} />
</View>
</Section>
<Section title="Financial Details" icon={FileText}>
<Section title="Financial Details" icon={FileText} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow
label="Estimated Value"
value={tenderData?.estimateValueInFig ? `₹ ${tenderData.estimateValueInFig}` : '-'}
styles={styles}
/>
<InfoRow
label="Estimated Value (Words)"
value={tenderData?.estimateValueInWord}
styles={styles}
/>
<InfoRow
label="Tender Fee"
value={tenderData?.tenderFeeInFig ? `₹ ${tenderData.tenderFeeInFig}` : '-'}
styles={styles}
/>
<InfoRow
label="EMD"
value={tenderData?.emdInFig ? `₹ ${tenderData.emdInFig}` : '-'}
styles={styles}
/>
<InfoRow label="GST %" value={tenderData?.gst} />
<InfoRow label="GST Amount" value={tenderData?.gstAmmount} />
<InfoRow label="GST %" value={tenderData?.gst} styles={styles} />
<InfoRow label="GST Amount" value={tenderData?.gstAmmount} styles={styles} />
</View>
</Section>
<Section title="Work & Validity" icon={Briefcase}>
<Section title="Work & Validity" icon={Briefcase} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow
label="Bid Validity Type"
value={tenderData?.bidValidateType}
styles={styles}
/>
<InfoRow
label="Bid Validity Period"
value={tenderData?.bidValidatePeriod}
styles={styles}
/>
<InfoRow
label="Work Completion Type"
value={tenderData?.workCompletionType}
styles={styles}
/>
<InfoRow
label="Work Completion Period"
value={tenderData?.workCompletionPeriod}
styles={styles}
/>
<InfoRow label="Rainy Season" value={tenderData?.rainySeason} />
<InfoRow label="Rainy Season" value={tenderData?.rainySeason} styles={styles} />
</View>
</Section>
<Section title="Key Dates" icon={Calendar}>
<Section title="Key Dates" icon={Calendar} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow
label="Purchase Start"
value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.tenderPurchaseDate?.startDateTime : tenderData?.keydates?.tenderPurchaseDate?.startDateTime
)}
styles={styles}
/>
<InfoRow
label="Purchase End"
value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.tenderPurchaseDate?.endDateTime : tenderData?.keydates?.tenderPurchaseDate?.endDateTime
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.tenderPurchaseDate?.endDateTime : tenderData?.keydates?.tenderPurchaseDate?.endDateTime
)}
styles={styles}
/>
<InfoRow
label="Bid Submission Start"
value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.startDateTime : tenderData?.keydates?.bidSubmissionDate?.startDateTime
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.startDateTime : tenderData?.keydates?.bidSubmissionDate?.startDateTime
)}
styles={styles}
/>
<InfoRow
label="Bid Submission End"
value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.endDateTime : tenderData?.keydates?.bidSubmissionDate?.endDateTime,
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.endDateTime : tenderData?.keydates?.bidSubmissionDate?.endDateTime,
)}
styles={styles}
/>
<InfoRow
label="Opening Envelope A"
value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEA : tenderData?.keydates?.openingEA
)}
styles={styles}
/>
<InfoRow
label="Opening Envelope B"
value={formatDateTime(tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEB : tenderData?.keydates?.openingEB)}
styles={styles}
/>
<InfoRow
label="Opening Envelope C"
value={formatDateTime(tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEC : tenderData?.keydates?.openingEC)}
styles={styles}
/>
</View>
</Section>
<Section title="Envelope Configuration" icon={Layers}>
<Section title="Envelope Configuration" icon={Layers} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow
label="Envelope Type"
value={tenderData?.envelopeType}
styles={styles}
/>
<InfoRow
label="Envelope A Enabled"
value={tenderData?.envelopeA ? 'Yes' : 'No'}
styles={styles}
/>
<InfoRow
label="Envelope B Enabled"
value={tenderData?.envelopeB ? 'Yes' : 'No'}
styles={styles}
/>
<InfoRow
label="Envelope C Enabled"
value={tenderData?.envelopeC ? 'Yes' : 'No'}
styles={styles}
/>
</View>
</Section>
<Section title="Department & Location" icon={Layers}>
<Section title="Department & Location" icon={Layers} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow label="Department" value={tenderData?.deptName} />
<InfoRow label="Circle" value={tenderData?.circle} />
<InfoRow label="Department" value={tenderData?.deptName} styles={styles} />
<InfoRow label="Circle" value={tenderData?.circle} styles={styles} />
<InfoRow
label="Division"
value={tenderData?.tenderSelectedDivision}
styles={styles}
/>
<InfoRow
label="Sub Division"
value={tenderData?.tenderSelectedSubDivision}
styles={styles}
/>
</View>
</Section>
<Section title="Officers" icon={Layers}>
<Section title="Officers" icon={Layers} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow label="Tender Master" value={tenderData?.tenderMaster} />
<InfoRow label="Tender Master" value={tenderData?.tenderMaster} styles={styles} />
{tenderData?.approvalOfficerNameEmail?.map((o: any, i: number) => (
<InfoRow
key={i}
label={`Approval Officer ${i + 1}`}
value={`${o.name} (${o.email})`}
styles={styles}
/>
))}
</View>
</Section>
<Section title="Documents" icon={Download}>
<Section title="Documents" icon={Download} styles={styles} colors={colors}>
<View style={styles.card}>
{tenderData?.tenderDocuments?.map((doc: any, i: number) => (
<View key={i} style={styles.documentRow}>
@@ -292,7 +456,7 @@ const LiveTenderDetail = () => {
<View style={styles.fileIcon}>
<FileText size={18} color={colors.primary} />
</View>
<Text style={styles.docName}>{doc.fileName}</Text>
<AppText variant="bodySmall" style={styles.docName}>{doc.fileName}</AppText>
</View>
<TouchableOpacity
style={styles.downloadBtn}
@@ -302,12 +466,12 @@ const LiveTenderDetail = () => {
<Download
size={18}
color={
downloadingId === doc?.docId ? '#9ca3af' : colors.primary
downloadingId === doc?.docId ? colors.textSecondary : colors.primary
}
/>
<Text style={styles.downloadText}>
<AppText variant="bodySmall" style={styles.downloadText}>
{downloadingId === doc?.docId ? 'Downloading...' : 'Download'}
</Text>
</AppText>
</TouchableOpacity>
</View>
))}
@@ -319,143 +483,3 @@ const LiveTenderDetail = () => {
};
export default LiveTenderDetail;
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#f8fafc',
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal:10,
borderBottomWidth: 1,
borderBottomColor: '#f1f5f9',
backgroundColor: '#fff',
},
backBtn: {
padding: 8,
marginRight: 8,
},
headerTitle: {
fontSize: 18,
fontWeight: '800',
color: '#0f172a',
},
headerSub: {
fontSize: 13,
color: '#64748b',
},
liveBox: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
borderWidth: 1,
borderColor: '#e2e8f0',
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
},
liveText: {
fontSize: 11,
fontWeight: '800',
color: '#475569',
},
content: {
padding: 16,
paddingBottom: 32,
},
sectionContainer: {
backgroundColor: '#ffffff',
borderRadius: 16,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: '#e2e8f0',
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
marginBottom: 12,
paddingBottom: 6,
borderBottomWidth: 1,
borderBottomColor: '#f8fafc',
},
sectionTitle: {
fontSize: 14,
fontWeight: '800',
color: '#1e293b',
textTransform: 'uppercase',
letterSpacing: 0.4,
},
card: {
backgroundColor: '#ffffff',
borderRadius: 12,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
paddingVertical: 8,
borderBottomWidth: 0.5,
borderBottomColor: '#e5e7eb',
},
label: {
fontSize: 13,
color: '#64748b',
flex: 1,
paddingRight: 8,
fontWeight: '600',
},
value: {
fontSize: 13,
color: '#111827',
flex: 1,
textAlign: 'right',
fontWeight: '500',
},
documentRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
gap: 10,
borderBottomWidth: 1,
borderBottomColor: '#f1f5f9',
},
docInfo: {
flexDirection: 'row',
alignItems: 'center',
flex: 1.4,
},
fileIcon: {
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: '#e0f2fe',
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
},
docName: {
flex: 1,
color: '#111827',
fontSize: 13,
fontWeight: '600',
},
downloadBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
downloadText: {
fontSize: 13,
fontWeight: '700',
color: colors.primary,
},
});
+155 -163
View File
@@ -1,13 +1,16 @@
import React, { memo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { View, StyleSheet, TouchableOpacity, Dimensions } 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 { Calendar, Tag, Info, ArrowRight, Clock, MapPin } from 'lucide-react-native';
import { useNavigation } from '@react-navigation/native';
import { format } from '../../Helper/DateFormat';
import { useTheme } from '../../Hooks/useTheme';
import LinearGradient from 'react-native-linear-gradient';
import AppText from '../../components/AppText';
import NavigationString from '../../Constant/NavigationString';
const { width } = Dimensions.get('window');
const LiveTenderTile: React.FC<LiveTenderTileProps> = ({
contractorId,
@@ -21,6 +24,8 @@ const LiveTenderTile: React.FC<LiveTenderTileProps> = ({
tenderNumber,
}) => {
const navigation: any = useNavigation();
const { colors, shadows, spacing } = useTheme();
const purchaseEnded = moment().isAfter(moment(purchaseEnd, 'YYYY-MM-DD HH:mm:ss'));
const contractorIds = typeof data?.contractorId === 'string'
@@ -30,227 +35,214 @@ const LiveTenderTile: React.FC<LiveTenderTileProps> = ({
: [];
const purchased = contractorIds.includes(String(contractorId));
if(data.tenderStatus==="Amended"){
}
const getStatusColor = () => {
if (isPurchased) return colors.error;
if (purchaseEnded) return colors.textSecondary;
if (purchased) return colors.textSecondary;
return colors.success;
};
const statusColor = getStatusColor();
return (
<View style={styles.cardWrapper}>
{/* Status Tag */}
<View
style={[
styles.statusTag,
{ backgroundColor: isPurchased ? colors.error : purchaseEnded ? '#94a3b8' : (purchased ? '#94a3b8' : '#4ade80') },
]}
/>
<TouchableOpacity
activeOpacity={0.9}
onPress={() => navigation.navigate('LiveTenderDetail', data)}
style={[styles.container, {
backgroundColor: colors.surface,
shadowColor: colors.shadow,
borderColor: colors.border
}]}
>
<TouchableOpacity
style={styles.container}
activeOpacity={0.9}
onPress={() => navigation.navigate('LiveTenderDetail', data)}
>
{/* Header */}
<View style={styles.header}>
<View style={styles.badge}>
<View style={styles.contentContainer}>
{/* Header Section */}
<View style={styles.headerRow}>
<View style={[styles.idTag, { backgroundColor: colors.background }]}>
<Tag size={14} color={colors.primary} />
<Text style={styles.badgeText}>#{tenderNumber}</Text>
<AppText variant="caption" style={{ color: colors.primary, fontWeight: '700' }}>
#{tenderNumber}
</AppText>
</View>
<Text style={styles.priceText}>₹{tenderFees}</Text>
<AppText variant="body" style={{ color: colors.primary }}>
₹{tenderFees}
</AppText>
</View>
{/* Work Title */}
<Text style={styles.workTitle} numberOfLines={2}>
{/* Title Section */}
<AppText
variant="body"
style={[styles.title, { color: colors.text }]}
numberOfLines={2}
>
{tenderOfWork ?? 'General Tender Work'}
</Text>
</AppText>
{/* Info Grid */}
<View style={styles.infoGrid}>
<View style={styles.infoItem}>
<Calendar size={16} color="#64748b" />
<View style={styles.infoTextGroup}>
<Text style={styles.infoLabel}>Start</Text>
<Text style={styles.infoValue}>{format.date6(`${tenderDispatchDate}`)}</Text>
<View style={[styles.infoContainer, { backgroundColor: colors.background }]}>
<View style={styles.infoRow}>
<View style={styles.infoCol}>
<View style={styles.iconBox}>
<Calendar size={16} color={colors.textSecondary} />
</View>
<View>
<AppText variant="caption" style={{ color: colors.textSecondary }}>Start Date</AppText>
<AppText variant='caption' style={{ color: colors.text, fontWeight: '600' }}>
{format.date6(`${tenderDispatchDate}`)}
</AppText>
</View>
</View>
</View>
<View style={styles.infoItem}>
<Calendar size={16} color={purchaseEnded ? "#ef4444" : "#22c55e"} />
<View style={styles.infoTextGroup}>
<Text style={styles.infoLabel}>Deadline</Text>
<Text style={[styles.infoValue, purchaseEnded && { color: '#ef4444' }]}>
{moment(purchaseEnd, 'YYYY-MM-DD HH:mm:ss').format('DD MMM, YY')}
</Text>
<View style={styles.divider} />
<View style={[styles.infoCol, { alignItems: 'flex-end' }]}>
<View style={styles.iconBox}>
<Clock size={16} color={purchaseEnded ? colors.error : colors.success} />
</View>
<View style={{ alignItems: 'flex-end' }}>
<AppText variant="caption" style={{ color: colors.textSecondary }}>Deadline</AppText>
<AppText variant="caption" style={{ color: purchaseEnded ? colors.error : colors.text, fontWeight: '600' }}>
{moment(purchaseEnd, 'YYYY-MM-DD HH:mm:ss').format('DD MMM, YY')}
</AppText>
</View>
</View>
</View>
</View>
{/* Action Bar */}
<View style={styles.actionBar}>
<TouchableOpacity style={styles.iconBtn} onPress={() => navigation.navigate('LiveTenderDetail', data)}>
<Info size={18} color={colors.primary} />
<Text style={styles.iconBtnText}>Details</Text>
{/* Actions Section */}
<View style={styles.actionRow}>
<TouchableOpacity
style={[styles.detailsBtn, { borderColor: colors.border }]}
onPress={() => navigation.navigate('LiveTenderDetail', data)}
>
<AppText variant="bodySmall" style={{ color: colors.textSecondary, fontWeight: '600' }}>
View Details
</AppText>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.mainButton,
purchaseEnded
? styles.btnDisabled
: (purchased ? styles.btnDisabled : isPurchased
? styles.btnPurchased
: styles.btnActive),
]}
onPress={onPurchasePress}
disabled={purchaseEnded || purchased}
onPress={isPurchased ? () => navigation.navigate(NavigationString.PurchaseTenderList) : onPurchasePress}
style={styles.actionBtnWrapper}
>
<Text
<View
style={[
styles.mainButtonText,
isPurchased && !purchaseEnded ? { color: colors.white } : { color: '#fff' },
styles.actionBtn,
{
backgroundColor: (purchaseEnded || purchased)
? colors.textSecondary
: isPurchased
? colors.success // Green for Go to Cart
: colors.primary // Orange for Add
}
]}
>
{purchaseEnded ? (purchased ? 'Purchased' : 'Expired') : (purchased ? 'Purchased' : isPurchased ? 'Remove' : 'Add to Cart')}
</Text>
{(!purchaseEnded && !purchased) && (
<ArrowRight size={16} color={isPurchased ? colors.white : '#fff'} />
)}
<AppText variant="button" style={{ color: '#fff', fontSize: 12 }}>
{purchaseEnded ? (purchased ? 'Purchased' : 'Closed') : (purchased ? 'Purchased' : isPurchased ? 'GO TO CART' : 'Add to Cart')}
</AppText>
{(!purchaseEnded && !purchased) && <ArrowRight size={14} color="#fff" />}
</View>
</TouchableOpacity>
</View>
</TouchableOpacity>
</View>
</View>
</TouchableOpacity>
);
};
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,
marginHorizontal: 16,
marginVertical: 10,
borderRadius: 20,
borderWidth: 1,
overflow: 'hidden',
elevation: 4,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
},
header: {
topLine: {
height: 4,
width: '100%',
},
contentContainer: {
padding: 16,
},
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
badge: {
idTag: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primary + '25',
paddingHorizontal: 12,
paddingVertical: 5,
borderRadius: 12,
paddingVertical: 4,
paddingHorizontal: 10,
borderRadius: 20,
gap: 6,
},
badgeText: {
fontSize: 12,
title: {
marginBottom: 16,
fontWeight: '700',
color: colors.primary,
},
priceText: {
fontSize: 20,
fontWeight: '800',
color: '#0f172a',
},
workTitle: {
fontSize: 16,
fontWeight: '600',
color: '#334155',
lineHeight: 24,
},
infoContainer: {
borderRadius: 16,
padding: 12,
marginBottom: 16,
},
infoGrid: {
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: '#f0f4f8',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 16,
marginBottom: 18,
alignItems: 'center',
},
infoItem: {
infoCol: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
flex: 1,
},
infoTextGroup: {
flexDirection: 'column',
divider: {
width: 1,
height: 30,
backgroundColor: 'rgba(0,0,0,0.05)',
marginHorizontal: 10,
},
infoLabel: {
fontSize: 10,
color: '#94a3b8',
textTransform: 'uppercase',
fontWeight: '700',
iconBox: {
// width: 32,
// height: 32,
// borderRadius: 16,
// alignItems: 'center',
// justifyContent: 'center',
// backgroundColor: 'rgba(0,0,0,0.03)',
},
infoValue: {
fontSize: 13,
fontWeight: '700',
color: '#1e293b',
},
actionBar: {
actionRow: {
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: {
detailsBtn: {
flex: 1,
height: 46,
borderRadius: 16,
paddingVertical: 10,
borderWidth: 1,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
actionBtnWrapper: {
flex: 1.5,
},
actionBtn: {
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',
paddingVertical: 10,
borderRadius: 14,
gap: 6,
},
});
@@ -7,7 +7,6 @@ import {
ScrollView,
SafeAreaView,
Platform,
StatusBar,
} from 'react-native';
import Modal from 'react-native-modal';
import moment from 'moment';
@@ -33,7 +32,7 @@ interface Props {
const TenderFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const inset = useSafeAreaInsets();
const { data: tenderResponse }: any = useFetchTenderById(data?.tenderId);
const tender = tenderResponse?.data?.[0];
@@ -74,8 +73,6 @@ const TenderFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
statusBarTranslucent
>
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" />
{/* Header */}
<View style={[styles.header, { paddingTop: inset.top }]}>
<TouchableOpacity onPress={onClose} style={styles.backBtn}>
+353 -420
View File
@@ -1,12 +1,24 @@
import { StyleSheet, Text, View, ScrollView, StatusBar, TouchableOpacity, RefreshControl, ActivityIndicator, Platform } from 'react-native'
import React, { useCallback, useEffect } from 'react'
import { StyleSheet, View, ScrollView, TouchableOpacity, RefreshControl, ActivityIndicator } from 'react-native'
import React, { useCallback } from 'react'
import { InsideHeader } from '../../Component/Index'
import colors from '../../Constant/Color'
import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'
import { useFetchTenderById, useProceedToBid } from '../../Services/ApiCall/Hooks'
import { useTheme } from '../../Hooks/useTheme'
import AppText from '../../components/AppText'
import { CircleDot, Clock, CheckCircle, XCircle, Calendar, FileText, Shield, AlertCircle } from 'lucide-react-native'
import Animated, { FadeInUp, FadeInDown, FadeIn, SlideInRight, Layout } from 'react-native-reanimated'
import { apiCall } from '../../Axios/Axios'
import NavigationString from '../../Constant/NavigationString'
import DeviceInfo from 'react-native-device-info'
import { BASE_URL } from '../../Services/ApiCall/ApiUrls'
import { useSelector } from 'react-redux'
import { useESetuAdharVarification } from '../../Services/ApiCall/Hooks'
import ConfirmDialog from '../../Constant/Dialog/ConfirmDialog'
import { generateTransactionId } from '../../Helper/uniqueId'
const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView)
const ProceedToBidDetail = () => {
// Sample data based on your JSON
const { colors, spacing, shadows, typography } = useTheme();
const route = useRoute<any>();
const tenderDataId = route.params?.tender;
const { data, refetch, isRefetching } = useProceedToBid(tenderDataId?.tenderId);
@@ -15,101 +27,95 @@ const ProceedToBidDetail = () => {
const [refreshing, setRefreshing] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const navigation = useNavigation<any>()
// Format date from array
const navigation = useNavigation<any>();
const user = useSelector((state: any) => state.token?.token);
const { mutateAsync: eSetuAdharVarification, isPending: isESetuPending } = useESetuAdharVarification();
const [showESetuDialog, setShowESetuDialog] = React.useState(false);
const [isESetuNavigating, setIsESetuNavigating] = React.useState(false);
const handleStartBidding = () => {
const handleStartBidding = async () => {
setLoading(true);
try {
setLoading(true);
navigation.navigate('TenderBidding', { tenderId: tenderDataId.tenderId });
const deviceId = await DeviceInfo.getUniqueId();
const url = `${BASE_URL}tenderBidding/getDsc-with-device-details?deviceId=${deviceId}`;
const response: any = await apiCall('get', url);
console.log("Response: ", response);
const { dsc, deviceBinding, deviceVerification, dscExpiryStatus } = response?.data || {};
if (!dsc) {
navigation.navigate(NavigationString.DscBinding, {
message: "Please map your DSC from the web screen"
});
} else if (!dscExpiryStatus) {
navigation.navigate(NavigationString.DscBinding, {
message: "Your DSC has been expired. Please map new DSC from the web screen"
});
} else if (deviceBinding && deviceVerification === 'PENDING') {
navigation.navigate(NavigationString.DeviceBinding, {
message: "Please approve this device to enable bidding access from the web.",
showBindButton: false
});
} else if (deviceBinding && deviceVerification === 'REVOKE') {
navigation.navigate(NavigationString.DeviceBinding, {
message: "Your device has been revoked. Please bind your device from the web portal.",
showBindButton: true
});
} else if (!deviceBinding) {
navigation.navigate(NavigationString.DeviceBinding, {
message: "Please bind your device from the web portal.",
showBindButton: true
});
} else {
// All checks passed (dsc is true, device is bound and approved)
// Now check Aadhaar verification (eSetu)
// setShowESetuDialog(true);
navigation.navigate(NavigationString.TenderBidding, {
tenderId: tenderDataId.tenderId
});
}
} catch (error) {
console.log("Error in starting bidding: ", error);
} finally {
setLoading(false);
}
}
const handleProceedESetu = async () => {
try {
setIsESetuNavigating(true);
const payload = {
contId: user.id,
transactionId: generateTransactionId(),
udf1: "23.77",
udf2: "77.34",
udf3: "string",
udf4: "string",
responseUrl: "http://dev.192.168.6.194:8090/"
};
const res = await eSetuAdharVarification(payload);
setShowESetuDialog(false);
// Pass returnRoute so KycWebView can come back here
navigation.navigate(NavigationString.KycWebViewScreen, {
res,
returnRoute: NavigationString.TenderBidding,
returnParams: { tenderId: tenderDataId.tenderId }
});
} catch (err) {
console.log('eSetu Error:', err);
setIsESetuNavigating(false);
} finally {
setIsESetuNavigating(false);
}
};
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 }: { status: boolean; label: string }) => (
<View style={styles.statusContainer}>
<View style={[styles.statusDot, { backgroundColor: status ? colors.success : colors.error }]} />
<Text style={[styles.statusText, { color: status ? colors.success : colors.error }]}>
{label}: {status ? "Submitted" : "Not Submitted"}
</Text>
</View>
);
// Envelope log card component
const EnvelopeLogCard = ({ log, title }: { log: any; title: string }) => (
<View style={styles.envelopeCard}>
<View style={styles.cardHeader}>
<Text style={styles.cardTitle}>{title}</Text>
<View style={[styles.envelopeBadge, { backgroundColor: colors.primary }]}>
<Text style={styles.envelopeBadgeText}>{log?.envelope}</Text>
</View>
</View>
<View style={styles.logDetailRow}>
<Text style={styles.label}>Activity:</Text>
<Text style={styles.value}>{log?.activity}</Text>
</View>
<View style={styles.logDetailRow}>
<Text style={styles.label}>Date & Time:</Text>
<Text style={styles.value}>{log?.dateTime}</Text>
</View>
<View style={styles.logDetailRow}>
<Text style={styles.label}>System Info:</Text>
<Text style={styles.value}>{log?.osName} / {log?.browserName}</Text>
</View>
<View style={styles.logDetailRow}>
<Text style={styles.label}>IP Address:</Text>
<Text style={styles.value}>{log?.ipAddress}</Text>
</View>
<View style={styles.logDetailRow}>
<Text style={styles.label}>Status Message:</Text>
<Text style={[styles.value, styles.successText]}>{log?.logMessage}</Text>
</View>
{log?.headingMsg && (
<View style={styles.logDetailRow}>
<Text style={styles.label}>Heading:</Text>
<Text style={styles.value}>{log?.headingMsg}</Text>
</View>
)}
</View>
);
// Information row component
const InfoRow = ({ label, value, isHighlighted = false }: { label: string; value: string; isHighlighted?: boolean }) => (
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{label}:</Text>
<Text style={[styles.infoValue, isHighlighted && styles.highlightedValue]}>{value}</Text>
</View>
);
// Feature chip component
const FeatureChip = ({ label, enabled }: { label: string; enabled: boolean }) => (
<View style={[styles.chip, { backgroundColor: enabled ? colors.successLight : colors.lightGray }]}>
<Text style={[styles.chipText, { color: enabled ? colors.success : colors.darkGray }]}>
{label} {enabled ? '✓' : '✗'}
</Text>
</View>
);
const onRefresh = async () => {
try {
setRefreshing(true);
@@ -125,483 +131,410 @@ const ProceedToBidDetail = () => {
}, [tenderData?.tenderId])
);
// Status indicator component with animation
const StatusIndicator = ({ status, label, index }: { status: boolean; label: string; index: number }) => (
<Animated.View
entering={FadeInUp.delay(100 + index * 80).springify()}
style={styles.statusContainer}
>
{status ? (
<CheckCircle size={18} color={colors.success} strokeWidth={2} />
) : (
<XCircle size={18} color={colors.error} strokeWidth={2} />
)}
<AppText variant="bodySmall" color={status ? colors.success : colors.error}>
{label}: {status ? "Submitted" : "Not Submitted"}
</AppText>
</Animated.View>
);
// Envelope log card component with animation
const EnvelopeLogCard = ({ log, title, index }: { log: any; title: string; index: number }) => (
<Animated.View
entering={SlideInRight.delay(100 + index * 100).springify()}
style={[styles.envelopeCard, { borderColor: colors.border }]}
>
<View style={styles.cardHeader}>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>{title}</AppText>
<View style={[styles.envelopeBadge, { backgroundColor: colors.primary }]}>
<AppText variant="caption" color={colors.buttonText} style={{ fontWeight: '700' }}>{log?.envelope}</AppText>
</View>
</View>
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>Activity:</AppText>
<AppText variant="caption" color={colors.text}>{log?.activity}</AppText>
</View>
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>Date & Time:</AppText>
<AppText variant="caption" color={colors.text}>{log?.dateTime}</AppText>
</View>
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>System Info:</AppText>
<AppText variant="caption" color={colors.text}>{log?.osName} / {log?.browserName}</AppText>
</View>
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>IP Address:</AppText>
<AppText variant="caption" color={colors.text}>{log?.ipAddress}</AppText>
</View>
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>Status Message:</AppText>
<AppText variant="caption" color={colors.success}>{log?.logMessage}</AppText>
</View>
{log?.headingMsg && (
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>Heading:</AppText>
<AppText variant="caption" color={colors.text}>{log?.headingMsg}</AppText>
</View>
)}
</Animated.View>
);
// Information row component with animation
const InfoRow = ({ label, value, isHighlighted = false, index }: { label: string; value: string; isHighlighted?: boolean; index: number }) => (
<Animated.View
entering={FadeInUp.delay(50 + index * 50).springify()}
style={[styles.infoRow, { borderBottomColor: colors.border }]}
>
<AppText variant="bodySmall" color={colors.textSecondary}>{label}:</AppText>
<AppText variant="bodySmall" color={isHighlighted ? colors.success : colors.text} style={{ fontWeight: '600' }}>{value}</AppText>
</Animated.View>
);
const styles = makeStyles(colors, spacing, shadows);
return (
<View style={{ flex: 1, backgroundColor: colors.white }}>
<View style={{ flex: 1, backgroundColor: colors.background }}>
<InsideHeader title="Tender Detail" showArrow />
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}
<AnimatedScrollView
style={styles.container}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[colors.primary]} // Android
tintColor={colors.primary} // iOS
colors={[colors.primary]}
tintColor={colors.primary}
/>
}>
}
>
{/* Tender Header Section */}
<View style={styles.headerCard}>
<Animated.View
entering={FadeInDown.duration(400).springify()}
layout={Layout.springify()}
style={[styles.headerCard, shadows.medium]}
>
<View style={styles.tenderHeader}>
<View>
<Text style={styles.tenderNumber}>{tenderData?.tenderNumber}</Text>
<Text style={styles.tenderId}>ID: {tenderData?.tenderId}</Text>
<AppText variant="h3" color={colors.primary} style={{ fontWeight: '700' }}>{tenderData?.tenderNumber}</AppText>
<AppText variant="caption" color={colors.textSecondary}>ID: {tenderData?.tenderId}</AppText>
</View>
<View style={[styles.statusBadge, { backgroundColor: tenderData?.biddingStatus ? colors.success : colors.warning }]}>
<Text style={styles.statusBadgeText}>
<Animated.View
entering={SlideInRight.delay(300).springify()}
style={[styles.statusBadge, { backgroundColor: tenderData?.biddingStatus ? colors.success : colors.primary }]}
>
<AppText variant="caption" color={colors.buttonText} style={{ fontWeight: '600' }}>
{tenderData?.biddingStatus ? "Active" : "Inactive"}
</Text>
</View>
</AppText>
</Animated.View>
</View>
<Text style={styles.workDescription}>{tenderData?.nameofWork}</Text>
<AppText variant="body" color={colors.text} style={{ marginTop: spacing.s, lineHeight: 22, fontSize: typography.size.s, color: colors.textSecondary }}>{tenderData?.nameofWork}</AppText>
<View style={styles.headerInfoRow}>
<View style={[styles.headerInfoRow, { backgroundColor: colors.surface }]}>
<View style={styles.headerInfoItem}>
<Text style={styles.headerInfoLabel}>NIT No.</Text>
<Text style={styles.headerInfoValue}>{tenderData?.nitNo}</Text>
<AppText variant="caption" color={colors.textSecondary}>NIT No.</AppText>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>{tenderData?.nitNo}</AppText>
</View>
<View style={styles.separator} />
<View style={[styles.separator, { backgroundColor: colors.border }]} />
<View style={styles.headerInfoItem}>
<Text style={styles.headerInfoLabel}>EMD Amount</Text>
<Text style={styles.headerInfoValue}>₹{tenderData?.emdInFig}</Text>
<AppText variant="caption" color={colors.textSecondary}>EMD Amount</AppText>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>₹{tenderData?.emdInFig}</AppText>
</View>
</View>
</View>
</Animated.View>
{/* Envelope Submission Status */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Envelope Submission Status</Text>
<View style={styles.statusGrid}>
<StatusIndicator status={tenderData?.envelopeASubmittStatus} label="Envelope A" />
{tenderData?.showEB &&<StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope B" />}
{tenderData?.showLCS &&<StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope LCS" />}
{tenderData?.showQCBS &&<StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope QCBS" />}
{tenderData?.showEC &&<StatusIndicator status={tenderData?.envelopeCSubmittStatus} label="Envelope C" />}
<Animated.View
entering={FadeInUp.delay(200).springify()}
style={styles.section}
>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Envelope Submission Status</AppText>
<View style={[styles.statusGrid, shadows.light]}>
<StatusIndicator status={tenderData?.envelopeASubmittStatus} label="Envelope A" index={0} />
{tenderData?.showEB && <StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope B" index={1} />}
{tenderData?.showLCS && <StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope LCS" index={2} />}
{tenderData?.showQCBS && <StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope QCBS" index={3} />}
{tenderData?.showEC && <StatusIndicator status={tenderData?.envelopeCSubmittStatus} label="Envelope C" index={4} />}
</View>
</View>
</Animated.View>
{/* Envelope Logs */}
{tenderData?.envelopeA_Logs && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Submission Logs</Text>
{tenderData?.envelopeA_Logs && <EnvelopeLogCard log={tenderData?.envelopeA_Logs} title="Envelope A Submission" />}
{tenderData?.envelopeB_Logs && <EnvelopeLogCard log={tenderData?.envelopeB_Logs} title="Envelope B Submission" />}
{tenderData?.envelopeC_Logs && <EnvelopeLogCard log={tenderData?.envelopeC_Logs} title="Envelope C Submission" />}
</View>
<Animated.View
entering={FadeInUp.delay(300).springify()}
style={styles.section}
>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Submission Logs</AppText>
{tenderData?.envelopeA_Logs && <EnvelopeLogCard log={tenderData?.envelopeA_Logs} title="Envelope A Submission" index={0} />}
{tenderData?.envelopeB_Logs && <EnvelopeLogCard log={tenderData?.envelopeB_Logs} title="Envelope B Submission" index={1} />}
{tenderData?.envelopeC_Logs && <EnvelopeLogCard log={tenderData?.envelopeC_Logs} title="Envelope C Submission" index={2} />}
</Animated.View>
)}
{/* Tender Details */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Tender Details</Text>
<View style={styles.detailsCard}>
<InfoRow label="Envelope Type" value={tenderData?.envelopeType} />
<InfoRow label="Tender Stages" value={tenderData?.tenderStages} />
<InfoRow label="Bid Withdrawal" value={tenderData?.bidWithDraw} isHighlighted={tenderData?.bidWithdrawStatus} />
<InfoRow label="Re-bid" value={tenderData?.reBid} />
<InfoRow label="DSC Status" value={tenderData?.dscStatus ? "Required" : "Not Required"} />
<Animated.View
entering={FadeInUp.delay(400).springify()}
style={styles.section}
>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Tender Details</AppText>
<View style={[styles.detailsCard, shadows.light]}>
<InfoRow label="Envelope Type" value={tenderData?.envelopeType} index={0} />
<InfoRow label="Tender Stages" value={tenderData?.tenderStages} index={1} />
<InfoRow label="Bid Withdrawal" value={tenderData?.bidWithDraw} isHighlighted={tenderData?.bidWithdrawStatus} index={2} />
<InfoRow label="Re-bid" value={tenderData?.reBid} index={3} />
<InfoRow label="DSC Status" value={tenderData?.dscStatus ? "Required" : "Not Required"} index={4} />
</View>
</View>
</Animated.View>
{/* Time Period */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Submission Time Period</Text>
<View style={styles.timeCard}>
<View style={styles.timeRow}>
<Text style={styles.timeLabel}>Starts On:</Text>
<Text style={styles.timeValue}>{formatDate(tenderData?.submittedStartDate)}</Text>
</View>
<View style={styles.timeRow}>
<Text style={styles.timeLabel}>Ends On:</Text>
<Text style={styles.timeValue}>{formatDate(tenderData?.submittedEndDate)}</Text>
</View>
<Animated.View
entering={FadeInUp.delay(500).springify()}
style={styles.section}
>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Submission Time Period</AppText>
<View style={[styles.timeCard, shadows.light]}>
<Animated.View
entering={FadeIn.delay(600)}
style={[styles.timeRow, { borderBottomColor: colors.border }]}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Calendar size={16} color={colors.primary} />
<AppText variant="bodySmall" color={colors.textSecondary}>Starts On:</AppText>
</View>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>{formatDate(tenderData?.submittedStartDate)}</AppText>
</Animated.View>
<Animated.View
entering={FadeIn.delay(700)}
style={styles.timeRow}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Calendar size={16} color={colors.error} />
<AppText variant="bodySmall" color={colors.textSecondary}>Ends On:</AppText>
</View>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>{formatDate(tenderData?.submittedEndDate)}</AppText>
</Animated.View>
</View>
</View>
{/* Feature Flags
<View style={styles.section}>
<Text style={styles.sectionTitle}>Available Features</Text>
<View style={styles.chipsContainer}>
<FeatureChip label="View Bid" enabled={tenderData?.viewBid} />
<FeatureChip label="Proceed to Bid" enabled={tenderData?.proceedtoBid} />
<FeatureChip label="Show Envelope B" enabled={tenderData?.showEB} />
<FeatureChip label="Show Envelope C" enabled={tenderData?.showEC} />
<FeatureChip label="QCBS" enabled={tenderData?.showQCBS} />
<FeatureChip label="LCS" enabled={tenderData?.showLCS} />
</View>
</View> */}
</Animated.View>
{/* Final Status */}
<View style={styles.finalStatusCard}>
<Text style={styles.finalStatusTitle}>Overall Status</Text>
<View style={styles.finalStatusRow}>
<Text style={styles.finalStatusLabel}>All Envelopes Submitted:</Text>
<Text style={[styles.finalStatusValue,
{
color: tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus
? colors.success : colors.error
}]}>
{tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus
? "COMPLETED" : "PENDING"}
</Text>
</View>
<View style={styles.finalStatusRow}>
<Text style={styles.finalStatusLabel}>Bidding Status:</Text>
<Text style={[styles.finalStatusValue,
{ color: tenderData?.biddingStatus ? colors.success : colors.error }]}>
<Animated.View
entering={FadeInUp.delay(600).springify()}
style={[styles.finalStatusCard, { borderColor: colors.primary }]}
>
<AppText variant="body" color={colors.primary} style={{ fontWeight: '700', marginBottom: spacing.m, textAlign: 'center' }}>Overall Status</AppText>
<Animated.View
entering={FadeIn.delay(700)}
style={[styles.finalStatusRow, { borderBottomColor: colors.border }]}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Shield size={16} color={colors.textSecondary} />
<AppText variant="bodySmall" color={colors.text}>All Envelopes Submitted:</AppText>
</View>
<AppText variant="bodySmall" color={tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus ? colors.success : colors.error} style={{ fontWeight: '700' }}>
{tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus ? "COMPLETED" : "PENDING"}
</AppText>
</Animated.View>
<Animated.View
entering={FadeIn.delay(800)}
style={styles.finalStatusRow}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<CircleDot size={16} color={colors.textSecondary} />
<AppText variant="bodySmall" color={colors.text}>Bidding Status:</AppText>
</View>
<AppText variant="bodySmall" color={tenderData?.biddingStatus ? colors.success : colors.error} style={{ fontWeight: '700' }}>
{tenderData?.biddingStatus ? "ACTIVE" : "CLOSED"}
</Text>
</View>
</View>
</AppText>
</Animated.View>
</Animated.View>
<View style={styles.footerSpace} />
</ScrollView>
</AnimatedScrollView>
<View style={styles.footer}>
{
tenderData?.proceedtoBid && !tenderData?.viewBid && tenderData?.biddingStatus && tenderData?.bidWithdrawStatus === false ? (
<Animated.View
entering={FadeInDown.duration(300)}
style={[styles.footer, { borderTopColor: colors.border }]}
>
{tenderData?.proceedtoBid && !tenderData?.viewBid && tenderData?.biddingStatus && tenderData?.bidWithdrawStatus === false ? (
<TouchableOpacity style={styles.proceedButton} onPress={handleStartBidding} disabled={isRefetching}>
{loading ? (
<ActivityIndicator size="small" color={colors.buttonText} />
) : (
<AppText variant="button" color={colors.buttonText}>Start Bidding</AppText>
)}
</TouchableOpacity>
) : (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
<AlertCircle size={16} color={colors.textSecondary} />
<AppText variant="bodySmall" color={colors.textSecondary}>One time bidding is completed</AppText>
</View>
)}
</Animated.View>
<TouchableOpacity style={styles.proceedButton} onPress={handleStartBidding} disabled={isRefetching}>
<Text style={styles.proceedButtonText}>{loading ? <ActivityIndicator size={'small'} color={'#ffff'} /> : 'Start Bidding'}</Text>
</TouchableOpacity>
) :
<Text style={styles.footerText}>One time bidding is completed</Text>
}
</View>
<ConfirmDialog
visible={showESetuDialog}
title="Aadhaar Verification Required"
message="To access bidding, please complete your Aadhaar verification."
confirmText={isESetuPending || isESetuNavigating ? "Processing..." : "Proceed"}
cancelText="Cancel"
onConfirm={handleProceedESetu}
onClose={() => !isESetuPending && !isESetuNavigating && setShowESetuDialog(false)}
loading={isESetuPending || isESetuNavigating}
/>
</View>
)
}
export default ProceedToBidDetail
const styles = StyleSheet.create({
const makeStyles = (colors: any, spacing: any, shadows: any) => StyleSheet.create({
container: {
flex: 1,
padding: 16,
marginBottom: 60,
padding: spacing.m,
},
headerCard: {
backgroundColor: colors.primaryLight,
borderRadius: 12,
padding: 20,
marginBottom: 20,
borderLeftWidth: 4,
borderLeftColor: colors.primary,
backgroundColor: colors.surface,
borderRadius: 16,
padding: spacing.m,
marginBottom: spacing.m,
},
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,
borderRadius: 10,
padding: spacing.s,
marginTop: spacing.m,
},
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,
marginBottom: spacing.m,
},
statusGrid: {
backgroundColor: colors.white,
backgroundColor: colors.surface,
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
padding: spacing.m,
},
statusContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
statusDot: {
width: 12,
height: 12,
borderRadius: 6,
marginRight: 10,
},
statusText: {
fontSize: 15,
fontWeight: '500',
marginBottom: spacing.s,
gap: 8,
},
envelopeCard: {
backgroundColor: colors.white,
backgroundColor: colors.surface,
borderRadius: 12,
padding: 16,
marginBottom: 12,
padding: spacing.m,
marginBottom: spacing.s,
borderWidth: 1,
borderColor: colors.lightGray,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
cardTitle: {
fontSize: 16,
fontWeight: '600',
color: colors.primary,
marginBottom: spacing.s,
},
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',
marginBottom: 6,
},
detailsCard: {
backgroundColor: colors.white,
backgroundColor: colors.surface,
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
padding: spacing.m,
},
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',
paddingVertical: spacing.s,
borderBottomWidth: StyleSheet.hairlineWidth,
},
timeCard: {
backgroundColor: colors.white,
backgroundColor: colors.surface,
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
padding: spacing.m,
},
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',
paddingVertical: spacing.s,
borderBottomWidth: StyleSheet.hairlineWidth,
},
finalStatusCard: {
backgroundColor: colors.primaryLight,
backgroundColor: colors.surface,
borderRadius: 12,
padding: 20,
marginTop: 10,
padding: spacing.m,
marginTop: spacing.s,
marginBottom: spacing.l,
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',
paddingVertical: spacing.s,
borderBottomWidth: StyleSheet.hairlineWidth,
},
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,
height: 80,
},
footer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: colors.white,
padding: 16,
backgroundColor: colors.surface,
padding: spacing.m,
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,
},
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,
},
}),
},
});
+447 -206
View File
@@ -1,255 +1,496 @@
import React, { useState } from 'react';
import { View, Text, FlatList, StyleSheet, TouchableOpacity, Alert } from 'react-native';
import {
View,
FlatList,
StyleSheet,
TouchableOpacity,
ActivityIndicator,
} from 'react-native';
import Animated, { FadeInDown, FadeIn } from 'react-native-reanimated';
import {
CreditCard,
Calendar,
Hash,
IndianRupee,
Percent,
Server,
Wifi,
WifiOff,
Receipt,
CheckCircle2,
} from 'lucide-react-native';
import { InsideHeader, Loader } from '../../Component/Index';
import colors from '../../Constant/Color';
import { CreditCard, Calendar, User, DollarSign, ArrowRight } from 'lucide-react-native';
import { useDispatch, useSelector } from 'react-redux';
import { useOfflineTenderPurchase } from '../../Services/ApiCall/Hooks';
import { StackActions, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
import { showErrorAlert, showToast } from '../../Constant/ShowDailog';
import { clearPurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const ProceedToPurTender = () => {
const navigation: any = useNavigation();
const Dispatch = useDispatch()
const [loading, setLoading] = useState(false);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender);
const navigation: any = useNavigation();
const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
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 { mutate, isPending } = useOfflineTenderPurchase();
const payableAmount =
Number(item.documentFees) + Number(item.portalCharge) + gstAmount;
return payableAmount
}
const calculateTotalAmount = (item: any) => {
const gstAmount =
(Number(item.documentFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100);
return Number(item.documentFees) + Number(item.portalCharge) + gstAmount;
};
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,
};
const grandTotal = SelectedPurchaseList.reduce(
(sum: number, item: any) => sum + calculateTotalAmount(item),
0
);
try {
mutate(body, {
onSuccess: (res) => {
if (res.statusCode === 200) {
showToast(res.message);
navigation.dispatch(StackActions.pop(2));
Dispatch(
clearPurchaseTender()
);
}
},
onError: (error) => {
showErrorAlert('Something went wrong');
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' ? showErrorAlert('Online Purchase is disabled') : handleOfflinePurchase();
}, 1200);
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,
};
const renderItem = ({ item }: { item: any[0] }) => (
<View style={styles.card}>
<View style={styles.cardHeader}>
<Text style={styles.title}>{item.auctionNumber}</Text>
<Text style={styles.pattern}>{item.auctionPattern}</Text>
</View>
try {
mutate(body, {
onSuccess: (res: any) => {
if (res.statusCode === 200) {
showToast(res.message);
navigation.dispatch(StackActions.pop(2));
Dispatch(clearPurchaseTender());
}
},
onError: (error: any) => {
showErrorAlert('Something went wrong');
console.log(error, '================purchse Error');
},
});
} catch (error) {
console.log('purchase offline error', error);
}
};
<View style={styles.cardRow}>
<Calendar size={18} color={colors.primary} />
<Text style={styles.text}>End Date: {item.purchaseEnd}</Text>
</View>
const handlePurchase = (type: 'online' | 'offline') => {
setLoading(true);
setTimeout(() => {
setLoading(false);
type === 'online'
? showErrorAlert('Online Purchase is disabled')
: handleOfflinePurchase();
}, 1200);
};
<View style={styles.cardRow}>
<User size={18} color={colors.primary} />
<Text style={styles.text}>Tender No: {item.tenderNumber}</Text>
</View>
const styles = makeStyles(colors, spacing, shadows);
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>Tender Fees: ₹{item.documentFees}</Text>
</View>
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>GST ({item.gst}%): ₹{item.gst / 100}</Text>
</View>
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>Portal Charge: ₹{item.portalCharge}</Text>
</View>
<View style={styles.totalRow}>
<CreditCard size={20} color={colors.white} />
<Text style={styles.total}>Total: ₹{calculateTotalAmount(item)}</Text>
</View>
</View>
);
const renderItem = ({ item, index }: { item: any; index: number }) => {
const total = calculateTotalAmount(item);
const gstAmount =
(Number(item.documentFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100);
return (
<View style={{ flex: 1, backgroundColor: colors.white }}>
<InsideHeader title="Tender Purchase" showArrow />
<View style={styles.screen}>
<FlatList
data={SelectedPurchaseList}
renderItem={renderItem}
keyExtractor={(item) => item?.tenderId?.toString()}
contentContainerStyle={styles.container}
/>
</View>
<Animated.View
entering={FadeInDown.delay(index * 90).springify()}
style={styles.card}
>
{/* Card Top Accent */}
<View style={styles.cardAccent} />
<View style={styles.bottomButtonContainer}>
<TouchableOpacity
style={[styles.button, { marginRight: 8 }]}
onPress={() => handlePurchase('online')}
>
<Text style={styles.buttonText}>Purchase Online</Text>
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, { marginLeft: 8 }]}
onPress={() => handlePurchase('offline')}
>
<Text style={styles.buttonText}>Purchase Offline</Text>
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} />
</TouchableOpacity>
<View style={styles.cardBody}>
{/* Header */}
<View style={styles.cardHeader}>
<View style={styles.indexBadge}>
<AppText variant="caption" color={colors.buttonText} style={{ fontWeight: '800' } as any}>
{String(index + 1).padStart(2, '0')}
</AppText>
</View>
{loading && (
<View style={styles.loadingOverlay}>
<Loader />
<View style={{ flex: 1, marginLeft: spacing.s }}>
<AppText variant="h4" color={colors.text} numberOfLines={2}>
{item.auctionOfWork || item.auctionNumber}
</AppText>
{item.auctionPattern ? (
<View style={styles.patternBadge}>
<AppText variant="caption" color={colors.primary} style={{ fontWeight: '600' } as any}>
{item.auctionPattern}
</AppText>
</View>
)}
) : null}
</View>
</View>
{/* Divider */}
<View style={styles.divider} />
{/* Info Rows */}
<InfoRow
icon={<Hash size={15} color={colors.primary} />}
label="Tender No"
value={item.tenderNumber}
colors={colors}
/>
<InfoRow
icon={<Calendar size={15} color={colors.primary} />}
label="Purchase End"
value={item.purchaseEnd || '—'}
colors={colors}
/>
{/* Divider */}
<View style={[styles.divider, { marginVertical: spacing.s }]} />
{/* Fee breakdown */}
<View style={styles.feeSection}>
<AppText variant="caption" color={colors.textSecondary} style={styles.feeHeader as any}>
FEE BREAKDOWN
</AppText>
<FeeRow
label="Document Fees"
value={`₹ ${Number(item.documentFees).toFixed(2)}`}
colors={colors}
/>
<FeeRow
label={`GST (${item.gst}%)`}
value={`₹ ${gstAmount.toFixed(2)}`}
colors={colors}
/>
<FeeRow
label="Portal Charge"
value={`₹ ${Number(item.portalCharge).toFixed(2)}`}
colors={colors}
/>
</View>
{/* Total Banner */}
<View style={styles.totalBanner}>
<View style={styles.totalLeft}>
<Receipt size={18} color={colors.buttonText} />
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '600' } as any}>
Total Payable
</AppText>
</View>
<AppText variant="h4" color={colors.buttonText}>
₹ {total.toFixed(2)}
</AppText>
</View>
</View>
</Animated.View>
);
};
return (
<View style={styles.screen}>
<InsideHeader title="Tender Purchase" showArrow />
{/* Grand Total Strip */}
<Animated.View entering={FadeInDown.springify()} style={styles.grandTotalStrip}>
<View>
<AppText variant="caption" color={colors.textSecondary}>
{SelectedPurchaseList.length} tender{SelectedPurchaseList.length !== 1 ? 's' : ''}
</AppText>
<AppText variant="h3" color={colors.text}>
₹ {grandTotal.toFixed(2)}
</AppText>
</View>
<View style={styles.grandTotalRight}>
<CheckCircle2 size={18} color={colors.success} />
<AppText variant="caption" color={colors.success} style={{ fontWeight: '600' } as any}>
Ready to pay
</AppText>
</View>
</Animated.View>
<FlatList
data={SelectedPurchaseList}
renderItem={renderItem}
keyExtractor={(item) => item?.tenderId?.toString()}
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
/>
{/* Bottom Action Buttons */}
<View style={styles.footer}>
<TouchableOpacity
style={[styles.button, styles.onlineButton]}
activeOpacity={0.85}
onPress={() => handlePurchase('online')}
>
<Wifi size={18} color={colors.primary} />
<AppText variant="button" color={colors.primary} style={styles.onlineBtnText as any}>
Online
</AppText>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.offlineButton]}
activeOpacity={0.85}
onPress={() => handlePurchase('offline')}
>
<WifiOff size={18} color={colors.buttonText} />
<AppText variant="button" color={colors.buttonText} style={styles.offlineBtnText as any}>
Offline
</AppText>
</TouchableOpacity>
</View>
{/* Loading Overlay */}
{(loading || isPending) && (
<Animated.View entering={FadeIn.duration(200)} style={styles.loadingOverlay}>
<View style={styles.loadingCard}>
<ActivityIndicator size="large" color={colors.primary} />
<AppText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: spacing.s } as any}>
Processing purchase…
</AppText>
</View>
</Animated.View>
)}
</View>
);
};
export default ProceedToPurTender;
const styles = StyleSheet.create({
/* ─── Sub-components ─── */
const InfoRow = ({
icon,
label,
value,
colors,
}: {
icon: React.ReactNode;
label: string;
value: string;
colors: any;
}) => (
<View style={infoRowStyles.row}>
<View style={infoRowStyles.iconWrap}>{icon}</View>
<AppText variant="caption" color={colors.textSecondary} style={infoRowStyles.label as any}>
{label}
</AppText>
<AppText variant="bodySmall" color={colors.text} style={infoRowStyles.value as any}>
{value}
</AppText>
</View>
);
const infoRowStyles = StyleSheet.create({
row: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 5,
gap: 6,
},
iconWrap: {
width: 22,
alignItems: 'center',
},
label: {
flex: 1,
fontWeight: '500',
} as any,
value: {
fontWeight: '600',
} as any,
});
const FeeRow = ({
label,
value,
colors,
}: {
label: string;
value: string;
colors: any;
}) => (
<View style={feeRowStyles.row}>
<AppText variant="caption" color={colors.textSecondary}>
{label}
</AppText>
<AppText variant="caption" color={colors.text} style={{ fontWeight: '600' } as any}>
{value}
</AppText>
</View>
);
const feeRowStyles = StyleSheet.create({
row: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 3,
},
});
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
screen: {
flex: 1,
flex: 1,
backgroundColor: colors.background,
},
grandTotalStrip: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: colors.surface,
marginHorizontal: spacing.m,
marginTop: spacing.m,
marginBottom: spacing.s,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
borderRadius: 18,
...shadows.medium,
},
grandTotalRight: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
backgroundColor: `${colors.success}15`,
paddingHorizontal: spacing.s + 2,
paddingVertical: spacing.xs + 2,
borderRadius: 20,
},
container: {
padding: 16,
paddingBottom: 120,
paddingHorizontal: spacing.m,
paddingTop: spacing.s,
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,
backgroundColor: colors.surface,
borderRadius: 20,
marginBottom: spacing.m,
overflow: 'hidden',
...shadows.medium,
},
cardAccent: {
height: 4,
backgroundColor: colors.primary,
},
cardBody: {
padding: spacing.m,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 12,
flexDirection: 'row',
alignItems: 'flex-start',
marginBottom: spacing.s,
},
title: {
fontSize: 16,
fontWeight: '700',
color: '#0F172A',
indexBadge: {
width: 32,
height: 32,
borderRadius: 10,
backgroundColor: colors.primary,
alignItems: 'center',
justifyContent: 'center',
marginTop: 2,
},
pattern: {
fontSize: 14,
color: colors.primary,
fontWeight: '600',
patternBadge: {
alignSelf: 'flex-start',
backgroundColor: `${colors.primary}15`,
paddingHorizontal: spacing.s,
paddingVertical: 2,
borderRadius: 8,
marginTop: 4,
},
cardRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
marginBottom: 4,
divider: {
height: StyleSheet.hairlineWidth,
backgroundColor: colors.border,
marginVertical: spacing.s,
},
text: {
fontSize: 14,
color: '#555',
feeSection: {
backgroundColor: colors.overlay,
borderRadius: 12,
padding: spacing.s + 2,
marginBottom: spacing.s,
},
totalRow: {
marginTop: 10,
backgroundColor: colors.primary,
borderRadius: 12,
paddingVertical: 10,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: 8,
feeHeader: {
letterSpacing: 1.2,
marginBottom: spacing.xs + 2,
fontWeight: '700',
} as any,
totalBanner: {
backgroundColor: colors.primary,
borderRadius: 14,
paddingVertical: 12,
paddingHorizontal: spacing.m,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
total: {
fontSize: 15,
fontWeight: '700',
color: '#fff',
totalLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
},
bottomButtonContainer: {
position: 'absolute',
bottom: 0,
left: 0,
flexDirection: 'row',
backgroundColor: colors.white,
width: '100%',
paddingVertical: 20,
paddingHorizontal: 10,
elevation: 5
footer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
flexDirection: 'row',
paddingHorizontal: spacing.m,
paddingTop: spacing.s + 4,
paddingBottom: spacing.m + 4,
backgroundColor: colors.surface,
gap: spacing.s,
...shadows.strong,
},
button: {
flex: 1,
backgroundColor: colors.primary,
paddingVertical: 14,
borderRadius: 12,
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 14,
borderRadius: 16,
gap: spacing.s,
},
buttonText: {
color: '#fff',
fontWeight: '700',
fontSize: 14,
onlineButton: {
backgroundColor: colors.surface,
borderWidth: 1.5,
borderColor: colors.primary,
},
offlineButton: {
backgroundColor: colors.buttonPrimary,
...shadows.medium,
},
onlineBtnText: {
letterSpacing: 1.2,
} as any,
offlineBtnText: {
letterSpacing: 1.2,
} as any,
loadingOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.2)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.3)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
},
});
loadingCard: {
backgroundColor: colors.surface,
borderRadius: 20,
padding: spacing.l,
alignItems: 'center',
...shadows.strong,
},
});
+3 -3
View File
@@ -34,7 +34,7 @@ const PurchaseTenderAllList = () => {
return () => clearTimeout(timer);
}
}, [isRefetching]);
const format = 'DD-MM-YYYY HH:mm:ss';
function isTenderActive(item: any): boolean {
@@ -70,10 +70,10 @@ const PurchaseTenderAllList = () => {
</View>
);
}, [isFetchingNextPage]);
return (
<View style={{ flex: 1 }}>
<SearchInput onChange={() => { }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} />
<SearchInput onChange={() => { }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} filter={false} />
{showRefreshBanner && (
<Animated.View
+370 -183
View File
@@ -1,223 +1,410 @@
import React, { useEffect, 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 React, { useState } from 'react';
import {
View,
StyleSheet,
ScrollView,
TouchableOpacity,
useWindowDimensions,
} from 'react-native';
import Animated, {
FadeIn,
FadeOut,
FadeInDown,
Layout,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { ChevronDown, ChevronRight, Trash2, ShoppingBag, FileText, Tag } 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 { togglePurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import { useEffect } from 'react';
const PurchaseTenderList = () => {
const Navigation: any = useNavigation()
const Dispatch = useDispatch()
const Navigation: any = useNavigation();
const Dispatch = useDispatch();
const { colors, spacing, shadows, typography } = useTheme();
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender);
const { width } = useWindowDimensions();
const toggleExpand = (index: number) => {
setExpandedIndex(prev => (prev === index ? null : index));
};
useEffect(()=>{
if(SelectedPurchaseList.length === 0){
Navigation.goBack()
}
},[SelectedPurchaseList]
useEffect(() => {
if (SelectedPurchaseList.length === 0) {
Navigation.goBack();
}
}, [SelectedPurchaseList]);
)
const styles = makeStyles(colors, spacing, shadows);
return (
<View style={styles.screen}>
<InsideHeader title="Purchase" showArrow />
<InsideHeader title="Purchase Tenders" showArrow />
<ScrollView contentContainerStyle={styles.container}>
{/* Cards */}
{SelectedPurchaseList.map((item: any, index: any) => {
const expanded = expandedIndex === index;
return (
<Animated.View
key={item.auctionId}
layout={Layout.springify()}
style={styles.card}
>
<TouchableOpacity
activeOpacity={0.9}
onPress={() => toggleExpand(index)}
>
<CardHeader
title={item.auctionOfWork}
expanded={expanded}
/>
{/* Summary Strip */}
<View style={styles.summaryStrip}>
<View style={styles.summaryIcon}>
<ShoppingBag size={16} color={colors.primary} />
</View>
<AppText variant="caption" color={colors.textSecondary}>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' } as any}>
{SelectedPurchaseList.length}
</AppText>
{' '}tender{SelectedPurchaseList.length !== 1 ? 's' : ''} selected for purchase
</AppText>
</View>
<Text style={styles.subTitle}>
Tender No: {item.tenderNumber}
</Text>
<ScrollView
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
>
{SelectedPurchaseList.map((item: any, index: number) => (
<TenderItem
key={item.auctionId || index}
item={item}
index={index}
expanded={expandedIndex === index}
toggleExpand={toggleExpand}
colors={colors}
styles={styles}
/>
))}
<Text style={styles.subTitle}>
Status:{' '}
<Text
style={{
color: item.tenderStatus === 'Opened' ? '#16A34A' : '#dc6f26ff',
fontWeight: '700',
}}
>
{item.tenderStatus === 'Opened' ? 'Open' : 'Approved'}
</Text>
</Text>
</TouchableOpacity>
{/* Expanded Content */}
{expanded && (
<Animated.View
entering={FadeIn.duration(220)}
exiting={FadeOut.duration(180)}
layout={Layout.springify()}
style={styles.details}
>
{/* {renderRow('', item.departmentName)} */}
{/* {renderRow('Pattern', item.auctionPattern)} */}
{renderRow('Fees', `₹ ${item.documentFees}`)}
{renderRow('Department', item.departmentName)}
{renderRow('Purchase End', item.purchaseEnd)}
<TouchableOpacity style={styles.removeButton} onPress={() => { Dispatch(togglePurchaseTender(item)) }}>
<Trash2 size={18} color="#fff" />
<Text style={styles.removeText}>Remove</Text>
</TouchableOpacity>
</Animated.View>
)}
</Animated.View>
);
})}
<View style={{ height: 90 }} />
</ScrollView>
{/* Footer */}
<TouchableOpacity disabled={Boolean(!SelectedPurchaseList.length)} style={[styles.proceedButton, { backgroundColor: Boolean(!SelectedPurchaseList.length) ? colors.lightText : colors.primary }]} onPress={() => { Navigation.navigate(NavigationString.ProceedToPurTender) }}>
<Text style={styles.proceedText}>Proceed to Purchase</Text>
</TouchableOpacity>
{/* Floating Proceed Button */}
<View style={styles.footerContainer}>
<TouchableOpacity
activeOpacity={0.9}
disabled={!SelectedPurchaseList.length}
style={[
styles.proceedButton,
{
backgroundColor: SelectedPurchaseList.length
? colors.buttonPrimary
: colors.secondary,
},
]}
onPress={() => Navigation.navigate(NavigationString.ProceedToPurTender)}
>
<AppText variant="button" color={colors.buttonText} style={styles.proceedText as any}>
Proceed to Purchase
</AppText>
<View style={styles.countBadge}>
<AppText variant="caption" color={colors.buttonPrimary} style={{ fontWeight: '800' } as any}>
{SelectedPurchaseList.length}
</AppText>
</View>
<ChevronRight size={20} color={colors.buttonText} />
</TouchableOpacity>
</View>
</View>
);
};
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` }],
}));
/* ─── Sub-components ─── */
return (
<View style={styles.cardHeader}>
<Text style={styles.title}>{title}</Text>
<Animated.View style={arrowStyle}>
<ChevronDown size={22} color={colors.black} />
</Animated.View>
</View>
);
};
const renderRow = (label: string, value: string) => (
<View style={styles.row}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.value}>{value}</Text>
const DetailRow = ({
label,
value,
colors,
valueColor,
}: {
label: string;
value: string;
colors: any;
valueColor?: string;
}) => (
<View style={detailRowStyles.row}>
<AppText variant="caption" color={colors.textSecondary} style={detailRowStyles.label as any}>
{label}
</AppText>
<AppText variant="caption" color={valueColor || colors.text} style={detailRowStyles.value as any}>
{value}
</AppText>
</View>
);
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: {
const detailRowStyles = StyleSheet.create({
row: {
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,
paddingVertical: 8,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0,0,0,0.06)',
},
label: {
fontSize: 13,
color: '#64748B',
},
flex: 1,
fontWeight: '500',
} as any,
value: {
fontSize: 15,
flex: 1,
textAlign: 'right',
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',
},
} as any,
});
/* ─── Chevron animated style hook ─── */
function useChevronStyle(expanded: boolean) {
const rotation = useSharedValue(expanded ? 1 : 0);
rotation.value = withTiming(expanded ? 1 : 0, { duration: 220 });
return useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value * 180}deg` }],
}));
}
/* ─── Extracted Tender Item Component ─── */
const TenderItem = ({ item, index, expanded, toggleExpand, colors, styles }: any) => {
const Dispatch = useDispatch();
const isOpen = item.tenderStatus === 'Opened';
const chevronStyle = useChevronStyle(expanded);
console.log(item, "itemmmmmmm");
return (
<Animated.View
entering={FadeInDown.delay(index * 80).springify()}
layout={Layout.springify()}
style={[styles.card, expanded && styles.cardExpanded]}
>
<View style={[styles.accentBar, { backgroundColor: isOpen ? colors.success : colors.primary }]} />
<View style={styles.cardInner}>
<TouchableOpacity
activeOpacity={0.85}
onPress={() => toggleExpand(index)}
>
<View style={styles.badgeRow}>
<View style={styles.tenderBadge}>
<FileText size={11} color={colors.primary} />
<AppText variant="caption" color={colors.primary} style={styles.badgeText as any}>
{item.tenderNumber}
</AppText>
</View>
</View>
<View style={styles.titleRow}>
<AppText variant="h4" style={styles.title as any} numberOfLines={2}>
{item.auctionOfWork}
</AppText>
<Animated.View style={chevronStyle}>
<ChevronDown size={20} color={colors.textSecondary} />
</Animated.View>
</View>
<View style={styles.deptRow}>
<Tag size={12} color={colors.textSecondary} />
<AppText variant="caption" color={colors.textSecondary} style={styles.deptText as any}>
{item.departmentName || 'Department'}
</AppText>
</View>
</TouchableOpacity>
{expanded && (
<Animated.View
entering={FadeIn.duration(220)}
exiting={FadeOut.duration(160)}
layout={Layout.springify()}
style={styles.detailsContainer}
>
<View style={styles.divider} />
<DetailRow
label="Document Fees"
value={`₹ ${item.documentFees}`}
colors={colors}
valueColor={colors.primary}
/>
<DetailRow
label="Department"
value={item.departmentName}
colors={colors}
/>
<DetailRow
label="Purchase End"
value={item.purchaseEnd}
colors={colors}
/>
<TouchableOpacity
style={styles.removeButton}
activeOpacity={0.85}
onPress={() => Dispatch(togglePurchaseTender(item))}
>
<Trash2 size={16} color="#fff" />
<AppText variant="button" color="#fff" style={styles.removeText as any}>
Remove
</AppText>
</TouchableOpacity>
</Animated.View>
)}
</View>
</Animated.View>
);
};
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
screen: {
flex: 1,
backgroundColor: colors.background,
},
summaryStrip: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.surface,
marginHorizontal: spacing.m,
marginTop: spacing.m,
marginBottom: spacing.s,
paddingHorizontal: spacing.m,
paddingVertical: spacing.s + 2,
borderRadius: 14,
gap: spacing.s,
...shadows.light,
},
summaryIcon: {
width: 28,
height: 28,
borderRadius: 8,
backgroundColor: `${colors.primary}15`,
alignItems: 'center',
justifyContent: 'center',
},
container: {
paddingHorizontal: spacing.m,
paddingTop: spacing.s,
},
card: {
backgroundColor: colors.surface,
borderRadius: 20,
marginBottom: spacing.m,
flexDirection: 'row',
overflow: 'hidden',
...shadows.medium,
},
cardExpanded: {
...shadows.strong,
},
accentBar: {
width: 5,
borderTopLeftRadius: 20,
borderBottomLeftRadius: 20,
},
cardInner: {
flex: 1,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
},
badgeRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: spacing.s,
},
tenderBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: `${colors.primary}15`,
paddingHorizontal: spacing.s,
paddingVertical: 3,
borderRadius: 8,
gap: 4,
},
badgeText: {
fontWeight: '600',
letterSpacing: 0.2,
} as any,
statusBadge: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.s + 2,
paddingVertical: 3,
borderRadius: 20,
gap: 5,
},
statusDot: {
width: 6,
height: 6,
borderRadius: 3,
},
titleRow: {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
gap: spacing.s,
},
title: {
flex: 1,
color: undefined, // will use AppText default from theme
fontWeight: '700',
} as any,
deptRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
marginTop: spacing.xs + 2,
},
deptText: {
letterSpacing: 0.3,
} as any,
detailsContainer: {
marginTop: spacing.s,
},
divider: {
height: 1,
backgroundColor: colors.border,
marginBottom: spacing.s,
},
removeButton: {
marginTop: spacing.m,
backgroundColor: colors.error,
borderRadius: 14,
paddingVertical: 12,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: spacing.s,
},
removeText: {
letterSpacing: 1,
} as any,
footerContainer: {
position: 'absolute',
bottom: 20,
left: spacing.m,
right: spacing.m,
},
proceedButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 16,
borderRadius: 20,
gap: spacing.s,
...shadows.strong,
},
proceedText: {
letterSpacing: 1.5,
} as any,
countBadge: {
backgroundColor: '#fff',
borderRadius: 12,
minWidth: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 6,
},
});
+172 -177
View File
@@ -1,268 +1,263 @@
import React, { use, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Platform } from 'react-native';
import React, { useMemo } from 'react';
import { View, StyleSheet, TouchableOpacity, Platform } 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 } from '../../Services/ApiCall/Hooks';
import { useNavigation } from '@react-navigation/native';
import TenderFullDetail from './Modal/TenderFullDetail';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
import { Calendar, Wallet, Banknote, ArrowRight, CircleDot, Clock } from 'lucide-react-native';
const PurchaseTenderTile = ({ item, onPressDetails }: any) => {
const scale = useSharedValue(1);
const elevation = useSharedValue(8);
const { colors, spacing, shadows } = useTheme();
const navigation = useNavigation<any>();
const scale = useSharedValue(1);
const elevation = useSharedValue(2); // Reduced initial elevation for a sleeker look
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
elevation: elevation.value,
}));
const { data: tenderResponse, }: any = useFetchTenderById(item?.tenderId);
const { data: tenderResponse }: any = useFetchTenderById(item?.tenderId); // Assuming id is reachable here, verified in original
// Fix: Original code used item?.tenderId, but useFetchTenderById might need checks.
// Keeping logic same as original: item contains tenderId?
// Original check: const { data: tenderResponse, }: any = useFetchTenderById(item?.tenderId);
const tender = tenderResponse?.data;
const handlePressIn = () => {
scale.value = withTiming(0.97, { duration: 120 });
elevation.value = withTiming(3, { duration: 120 });
scale.value = withTiming(0.98, { duration: 150 });
};
const handlePressOut = () => {
scale.value = withTiming(1, { duration: 160 });
elevation.value = withTiming(8, { duration: 160 });
scale.value = withTiming(1, { duration: 150 });
};
const isLive = item?.biddingStatus === true;
const statusColor = isLive ? colors.primary : '#94A3B8';
const lightStatusBg = isLive ? '#EFF6FF' : '#F8FAFC';
const [showDetail, setShowDetail] = React.useState(false);
// Dynamic colors based on status
const statusConfig = useMemo(() => {
if (isLive) {
return {
color: colors.primary,
bg: colors.primary + '15', // 15 = ~8% opacity
label: 'LIVE NOW',
icon: CircleDot
};
}
return {
color: colors.textSecondary,
bg: colors.secondary + '15',
label: 'CLOSED',
icon: Clock
};
}, [isLive, colors]);
const styles = useMemo(() => styling(colors, spacing), [colors, spacing]);
return (
<Animated.View
entering={FadeInUp.duration(400)}
style={[styles.shadowContainer, animatedStyle]}
entering={FadeInUp.duration(500).springify()}
style={[styles.container, shadows.light, animatedStyle]}
>
<TouchableOpacity
activeOpacity={1}
onPress={onPressDetails}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
style={styles.cardInternal}
style={styles.card}
>
<View style={[styles.statusAccent, { backgroundColor: statusColor }]} />
<View style={styles.mainInfo}>
{/* Header */}
<View style={styles.headerRow}>
<View>
<View style={[styles.statusBadge, { backgroundColor: lightStatusBg }]}>
<View style={[styles.statusDot, { backgroundColor: statusColor }]} />
<Text style={[styles.statusText, { color: statusColor }]}>
{isLive ? 'LIVE' : 'CLOSED'}
</Text>
</View>
<Text style={styles.title}>{item?.tenderNumber}</Text>
{/* Header Section */}
<View style={styles.header}>
<View style={styles.headerTopUser}>
<View style={[styles.statusBadge, { backgroundColor: statusConfig.bg }]}>
<statusConfig.icon size={12} color={statusConfig.color} strokeWidth={3} />
<AppText variant="caption" style={[styles.statusText, { color: statusConfig.color }]}>
{statusConfig.label}
</AppText>
</View>
<AppText variant="caption" color={colors.textSecondary} style={{ letterSpacing: 0.5 }}>
ID: {item?.tenderNumber}
</AppText>
</View>
<View style={styles.amountContainer}>
<Text style={styles.amount}>
<View style={{ marginTop: 8 }}>
<AppText variant="bodySmall" color={colors.text} numberOfLines={2}>
{tender?.workDescription || item?.tenderNumber || 'Tender Details'}
</AppText>
</View>
</View>
<View style={styles.divider} />
{/* Info Grid */}
<View style={styles.infoContainer}>
<View style={styles.infoRow}>
<View style={styles.iconBox}>
<Banknote size={18} color={colors.primary} />
</View>
<View>
<AppText variant="caption" color={colors.textSecondary}>Tender Fee</AppText>
<AppText variant="caption" style={{ fontWeight: '700' }}>
₹{Number(item?.tenderFeeInFig || 0).toLocaleString('en-IN')}
</Text>
<Text style={styles.feeLabel}>Tender Fee</Text>
</AppText>
</View>
</View>
{/* Date */}
<TouchableOpacity style={styles.dateRow} onPress={() => {
setShowDetail(true)
}}>
<Text style={styles.dateText}>
<Text style={styles.dateLabel}>Bid Ends: </Text>
{moment(
item?.keydates?.bidSubmissionDate?.endDateTime,
'DD-MM-YYYY HH:mm:ss'
).format('DD MMM YYYY, hh:mm A')}
</Text>
</TouchableOpacity>
{/* Footer */}
<View style={styles.footer}>
<View style={styles.footerTop}>
<Text style={styles.footerLabel}>Remaining Time</Text>
<View style={styles.infoRow}>
<View style={styles.iconBox}>
<Calendar size={18} color={colors.primary} />
</View>
<View>
<AppText variant="caption" color={colors.textSecondary}>Bid Ends</AppText>
<AppText variant="caption" style={{ fontWeight: '600' }}>
{moment(item?.keydates?.bidSubmissionDate?.endDateTime, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, hh:mm A')}
</AppText>
</View>
</View>
</View>
{/* Footer / Timer Action */}
<View style={styles.footer}>
<AppText variant="caption" color={colors.textSecondary} style={{ marginBottom: 4, alignSelf: 'flex-start' }}>
Time Remaining
</AppText>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
{/* Timer */}
<View style={styles.timerContainer}>
{isLive ? (
<CountdownTimer
endDate={
item?.keydates?.bidSubmissionDate?.endDateTime
}
endDate={item?.keydates?.bidSubmissionDate?.endDateTime}
/>
) : (
<Text style={styles.endedText}>--:--:--</Text>
<AppText variant="bodySmall" style={{ fontWeight: '700', color: colors.textSecondary }}>
-- : -- : --
</AppText>
)}
</View>
{/* CTA Button */}
<TouchableOpacity
disabled={!isLive}
onPress={() => {
if (isLive) {
if (isLive && tender) {
navigation.navigate('ProceedToBidDetail', { tender });
} else if (isLive && !tender) {
console.warn("Tender data possibly missing for navigation");
navigation.navigate('ProceedToBidDetail', { tender: item });
}
}}
activeOpacity={0.85}
style={[
styles.actionButton,
{ backgroundColor: statusColor },
!isLive && styles.disabledButton,
{ backgroundColor: isLive ? colors.primary : colors.border },
!isLive && styles.disabledButton
]}
>
<Text style={styles.addText}>
{isLive ? 'Proceed to Bid' : 'Tender Closed'}
</Text>
<AppText variant="button" color={isLive ? colors.buttonText : colors.textSecondary} style={{ marginRight: 8 }}>
{isLive ? 'Bid Now' : 'Closed'}
</AppText>
{isLive && <ArrowRight size={16} color={colors.buttonText} />}
</TouchableOpacity>
</View>
</View>
{/* Tender Detail Modal */}
<TenderFullDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} />
</TouchableOpacity>
</Animated.View>
);
};
export default PurchaseTenderTile;
const styles = StyleSheet.create({
shadowContainer: {
marginHorizontal: 5,
marginTop: 14,
marginBottom: 6,
const styling = (colors: any, spacing: any) => StyleSheet.create({
container: {
marginHorizontal: spacing.s,
marginBottom: spacing.m,
borderRadius: 16,
backgroundColor: '#fff',
elevation: 8,
backgroundColor: colors.surface,
},
cardInternal: {
card: {
borderRadius: 16,
overflow: 'hidden',
flexDirection: 'row',
backgroundColor: '#fff',
backgroundColor: colors.surface,
padding: spacing.m,
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.03)',
},
statusAccent: {
width: 5,
height: '100%',
header: {
marginBottom: spacing.m,
},
mainInfo: {
flex: 1,
padding: 16,
},
headerRow: {
headerTopUser: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 12,
alignItems: 'center',
},
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,
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
gap: 6
},
statusText: {
fontWeight: '700',
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',
divider: {
height: 1,
backgroundColor: colors.border,
marginBottom: spacing.m,
opacity: 0.5
},
actionButton: {
width: '100%',
height: 48,
borderRadius: 12,
infoContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: spacing.l,
gap: spacing.s
},
infoRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
flex: 1,
},
iconBox: {
width: 36,
height: 36,
borderRadius: 10,
backgroundColor: colors.primary + '15', // light opacity primary
justifyContent: 'center',
alignItems: 'center',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
}),
},
footer: {
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: colors.background, // Slightly distinct bg for footer
marginHorizontal: -spacing.m,
marginBottom: -spacing.m,
padding: spacing.m,
borderBottomLeftRadius: 16,
borderBottomRightRadius: 16,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.border,
},
timerContainer: {
flex: 1,
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 12,
},
disabledButton: {
backgroundColor: '#E2E8F0',
},
addText: {
color: '#fff',
fontSize: 14,
fontWeight: '800',
letterSpacing: 0.2,
},
opacity: 0.7
}
});
export default PurchaseTenderTile;
+281 -240
View File
@@ -1,7 +1,6 @@
import React, { useState, useRef, useMemo } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Animated,
@@ -12,40 +11,39 @@ import {
useFetchLiveTender,
useFetchPurchaseTender,
} from '../../Services/ApiCall/Hooks';
import colors from '../../Constant/Color';
import { ShoppingCart, Lock } from 'lucide-react-native';
import { ShoppingCart, Lock, Radio, Package } 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';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const TAB_HEIGHT = 44;
const TAB_HEIGHT = 48;
const HORIZONTAL_PADDING = 32;
const SWITCH_INSETS = 2;
const SWITCH_INSETS = 3;
const Tender = () => {
const { colors, spacing, shadows } = useTheme();
const Navigation = useNavigation() as any;
const { data: liveTenderData }: any = useFetchLiveTender();
const { data: purchaseTenderData }: any = useFetchPurchaseTender();
const SelectedPurchaseList = useSelector(
(state: any) => state?.selectPurchaseTender,
);
const SelectedPurchaseList = useSelector((state: any) => state?.selectPurchaseTender);
const reduxToken = useSelector((state: any) => state.token?.token);
const liveCount = liveTenderData?.pages?.[0]?.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 TAB_WIDTH = useMemo(() => {
const availableWidth = SCREEN_WIDTH - HORIZONTAL_PADDING - SWITCH_INSETS * 2;
return availableWidth / 2.4;
return availableWidth / 2;
}, []);
const [selectedTab, setSelectedTab] = useState<'live' | 'purchase'>('purchase');
const translateX = useRef(new Animated.Value(TAB_WIDTH)).current;
const onTabPress = (tab: 'live' | 'purchase', index: number) => {
setSelectedTab(tab);
Animated.spring(translateX, {
@@ -55,252 +53,295 @@ const Tender = () => {
}).start();
};
// Check if user is auction type
const isAuctionUser = reduxToken?.userType?.toLowerCase() === 'auction';
const cartCount = SelectedPurchaseList.length;
// Render restricted access message
const renderRestrictedAccess = () => (
<View style={styles.restrictedContainer}>
<View style={styles.restrictedCard}>
<View style={styles.iconContainer}>
<Lock size={64} color={colors.primary} />
const styles = makeStyles(colors, spacing, shadows, TAB_WIDTH, TAB_HEIGHT);
if (isAuctionUser) {
return (
<View style={styles.container}>
<InsideHeader title="Tender" showArrow />
<View style={styles.restrictedContainer}>
<View style={styles.restrictedCard}>
{/* Double-ring Lock Icon */}
<View style={styles.lockRing}>
<View style={styles.lockInner}>
<Lock size={40} color={colors.primary} strokeWidth={1.8} />
</View>
</View>
<AppText variant="h2" centered color={colors.text} style={styles.restrictedTitle as any}>
Access Restricted
</AppText>
<AppText variant="body" centered color={colors.textSecondary} style={styles.restrictedMessage as any}>
You're logged in as an{' '}
<AppText variant="body" color={colors.primary} style={{ fontWeight: '700' } as any}>
Auction user
</AppText>
{'. '}Tender features are not available for your account type.
</AppText>
<View style={styles.hintBox}>
<AppText variant="bodySmall" centered color={colors.textSecondary}>
Switch to a Tender account to access live & purchase tender features.
</AppText>
</View>
<TouchableOpacity
style={styles.loginButton}
activeOpacity={0.85}
onPress={() => Navigation.navigate(NavigationString.Login)}
>
<AppText variant="button" color={colors.buttonText} style={styles.loginBtnText as any}>
Switch to Tender Login
</AppText>
</TouchableOpacity>
</View>
</View>
<Text style={styles.restrictedTitle}>Tender Access Restricted</Text>
<Text style={styles.restrictedMessage}>
You are currently logged in as an Auction user. Tender features are not available for your account type.
</Text>
<Text style={styles.restrictedSubMessage}>
To access tender features, please login with a Tender user account.
</Text>
<TouchableOpacity
style={styles.loginButton}
onPress={() => Navigation.navigate(NavigationString.Login)}
>
<Text style={styles.loginButtonText}>Switch to Tender Login</Text>
</TouchableOpacity>
</View>
</View>
);
);
}
return (
<View style={styles.container}>
<InsideHeader title="Tender" showArrow />
{isAuctionUser ? (
// Show restricted access message for auction users
renderRestrictedAccess()
) : (
// Show normal tender UI for tender users
<>
<View style={styles.topRow}>
<View
{/* Tab Row */}
<View style={styles.topRow}>
{/* Segmented Control */}
<View
style={[
styles.switchWrapper,
{ width: TAB_WIDTH * 2 + SWITCH_INSETS * 2 },
]}
>
{/* Sliding pill */}
<Animated.View
pointerEvents="none"
style={[
styles.activeTab,
{ width: TAB_WIDTH, transform: [{ translateX }] },
]}
/>
{/* Live Tab */}
<TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8}
onPress={() => onTabPress('live', 0)}
>
<Radio
size={13}
color={selectedTab === 'live' ? colors.primary : colors.textSecondary}
strokeWidth={2}
/>
<AppText
variant="caption"
color={selectedTab === 'live' ? colors.primary : colors.textSecondary}
style={[
styles.switchWrapper,
{
width: TAB_WIDTH * 2 + SWITCH_INSETS * 2,
},
styles.tabText,
selectedTab === 'live' && (styles.activeTabText as any),
]}
>
<Animated.View
pointerEvents="none"
style={[
styles.activeTab,
{
width: TAB_WIDTH,
transform: [{ translateX }],
},
]}
/>
Live {liveCount ? `(${liveCount})` : ''}
</AppText>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8}
onPress={() => onTabPress('live', 0)}
>
<Text
style={[
styles.tabText,
selectedTab === 'live' && styles.activeText,
]}
>
Live ({liveCount})
</Text>
</TouchableOpacity>
{/* Purchase Tab */}
<TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8}
onPress={() => onTabPress('purchase', 1)}
>
<Package
size={13}
color={selectedTab === 'purchase' ? colors.primary : colors.textSecondary}
strokeWidth={2}
/>
<AppText
variant="caption"
color={selectedTab === 'purchase' ? colors.primary : colors.textSecondary}
style={[
styles.tabText,
selectedTab === 'purchase' && (styles.activeTabText as any),
]}
>
Purchased {purchaseCount ? `(${purchaseCount})` : ''}
</AppText>
</TouchableOpacity>
</View>
</View>
<TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8}
onPress={() => onTabPress('purchase', 1)}
>
<Text
style={[
styles.tabText,
selectedTab === 'purchase' && styles.activeText,
]}
>
Purchased ({purchaseCount})
</Text>
</TouchableOpacity>
</View>
{/* Content */}
{selectedTab === 'live' ? <LiveTender /> : <PurchaseTenderAllList />}
<View>
{SelectedPurchaseList.length ? (
<View
style={{
borderWidth: 2,
borderColor: colors.white,
backgroundColor: colors.primaryLight,
position: 'absolute',
borderRadius: 50,
zIndex: 20,
right: 0,
top: -10,
paddingHorizontal: 2,
}}
>
<Text style={{ fontSize: 12, color: colors.black }}>
{SelectedPurchaseList.length}
</Text>
</View>
) : null}
<TouchableOpacity
style={{
padding: 10,
backgroundColor: colors.primaryLight,
borderRadius: 20,
}}
onPress={() => {
Navigation.navigate(NavigationString.PurchaseTenderList);
}}
>
<ShoppingCart size={22} color={colors.black} strokeWidth={2} />
</TouchableOpacity>
</View>
{/* Cart Floating Button */}
<TouchableOpacity
style={styles.cartFloatingButton}
activeOpacity={0.9}
onPress={() => Navigation.navigate(NavigationString.PurchaseTenderList)}
>
<ShoppingCart size={24} color={colors.buttonText} strokeWidth={2} />
{cartCount > 0 && (
<View style={styles.cartBadge}>
<AppText variant="caption" color={colors.primary} style={styles.cartBadgeText as any}>
{cartCount}
</AppText>
</View>
{selectedTab === 'live' ? <LiveTender /> : <PurchaseTenderAllList />}
</>
)}
)}
</TouchableOpacity>
</View>
);
};
export default Tender;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.white,
},
topRow: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
paddingHorizontal: 16,
},
switchWrapper: {
flexDirection: 'row',
marginVertical: 14,
backgroundColor: '#f1f5f9',
borderRadius: 18,
padding: 2,
height: TAB_HEIGHT,
},
tab: {
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,
height: TAB_HEIGHT - 4,
backgroundColor: '#fff',
borderRadius: 16,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowRadius: 6,
elevation: 3,
},
restrictedContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 24,
backgroundColor: colors.lightGray,
},
restrictedCard: {
backgroundColor: colors.white,
borderRadius: 24,
padding: 32,
alignItems: 'center',
maxWidth: 400,
width: '100%',
},
iconContainer: {
width: 120,
height: 120,
borderRadius: 60,
backgroundColor: colors.primaryHighlight,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 24,
},
restrictedTitle: {
fontSize: 24,
fontWeight: '700',
color: colors.darkText,
textAlign: 'center',
marginBottom: 16,
fontFamily: FontsFamily.poppinsBold,
},
restrictedMessage: {
fontSize: 16,
color: colors.grayText,
textAlign: 'center',
lineHeight: 24,
marginBottom: 12,
fontFamily: FontsFamily.poppinsRegular,
},
restrictedSubMessage: {
fontSize: 14,
color: colors.grayText,
textAlign: 'center',
lineHeight: 22,
marginBottom: 32,
fontFamily: FontsFamily.poppinsRegular,
},
loginButton: {
backgroundColor: colors.primary,
paddingHorizontal: 32,
paddingVertical: 14,
borderRadius: 12,
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
},
loginButtonText: {
color: colors.white,
fontSize: 16,
fontWeight: '600',
fontFamily: FontsFamily.poppinsSemiBold,
},
});
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any, TAB_WIDTH: number, TAB_HEIGHT: number) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
/* ── Tab Row ── */
topRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: spacing.m,
paddingVertical: spacing.s + 2,
},
switchWrapper: {
flexDirection: 'row',
backgroundColor: colors.surface,
borderRadius: 16,
padding: SWITCH_INSETS,
height: TAB_HEIGHT,
...shadows.light,
},
tab: {
height: TAB_HEIGHT - SWITCH_INSETS * 2,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 5,
zIndex: 2,
},
tabText: {
fontWeight: '600',
letterSpacing: 0.3,
} as any,
activeTabText: {
fontWeight: '700',
} as any,
activeTab: {
position: 'absolute',
top: SWITCH_INSETS,
left: SWITCH_INSETS,
height: TAB_HEIGHT - SWITCH_INSETS * 2,
backgroundColor: colors.background,
borderRadius: 13,
...shadows.medium,
},
/* ── Cart Floating Button ── */
cartFloatingButton: {
position: 'absolute',
bottom: spacing.xl + 80,
right: spacing.l,
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: colors.primary,
alignItems: 'center',
justifyContent: 'center',
...shadows.strong,
zIndex: 999,
},
cartBadge: {
position: 'absolute',
top: 0,
right: 0,
minWidth: 20,
height: 20,
borderRadius: 10,
backgroundColor: colors.background,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 4,
borderWidth: 1.5,
borderColor: colors.primary,
},
cartBadgeText: {
fontSize: 10,
fontWeight: '800',
lineHeight: 13,
} as any,
/* ── Restricted Access ── */
restrictedContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.l,
backgroundColor: colors.background,
},
restrictedCard: {
backgroundColor: colors.surface,
borderRadius: 28,
padding: spacing.l,
alignItems: 'center',
width: '100%',
maxWidth: 400,
...shadows.strong,
},
lockRing: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: `${colors.primary}12`,
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.l,
borderWidth: 1.5,
borderColor: `${colors.primary}25`,
},
lockInner: {
width: 72,
height: 72,
borderRadius: 36,
backgroundColor: `${colors.primary}18`,
alignItems: 'center',
justifyContent: 'center',
},
restrictedTitle: {
marginBottom: spacing.s + 4,
} as any,
restrictedMessage: {
marginBottom: spacing.s,
lineHeight: 24,
} as any,
hintBox: {
backgroundColor: colors.overlay,
borderRadius: 14,
paddingHorizontal: spacing.m,
paddingVertical: spacing.s + 2,
marginBottom: spacing.l,
width: '100%',
},
loginButton: {
backgroundColor: colors.buttonPrimary,
paddingHorizontal: spacing.l,
paddingVertical: 14,
borderRadius: 16,
width: '100%',
alignItems: 'center',
...shadows.medium,
},
loginBtnText: {
letterSpacing: 1.2,
} as any,
});
File diff suppressed because it is too large Load Diff