updated code with dsc and device binding
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user