updated code with dsc and device binding

This commit is contained in:
Abin Toppo
2026-05-08 13:39:41 +05:30
parent c1ecc97e29
commit 66461370a2
120 changed files with 11910 additions and 8347 deletions
+286 -236
View File
@@ -1,22 +1,29 @@
import React, { useState, useRef, useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Animated, Dimensions } from 'react-native';
import {
View,
StyleSheet,
TouchableOpacity,
Animated,
Dimensions,
} from 'react-native';
import { InsideHeader } from '../../Component/Index';
import { useFetchLiveAuction, useFetchPurchaseAuction } from '../../Services/ApiCall/Hooks';
import colors from '../../Constant/Color';
import LiveAuction from './LiveAuction';
import PurchaseAuction from './PurchaseAuction';
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 { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const HORIZONTAL_PADDING = 32;
const SWITCH_INSETS = 2;
const TAB_HEIGHT = 44;
const SWITCH_INSETS = 3;
const TAB_HEIGHT = 48;
const Auction = () => {
const { colors, spacing, shadows } = useTheme();
const Navigation = useNavigation() as any;
const { data: liveAuctionData = {} }: any = useFetchLiveAuction();
const { data: purchaseAuctionData = {} } = useFetchPurchaseAuction();
@@ -31,14 +38,14 @@ const Auction = () => {
? purchaseAuctionData.data.length
: 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, {
@@ -48,252 +55,295 @@ const Auction = () => {
}).start();
};
// Check if user is tender type
const isTenderUser = reduxToken?.userType?.toLowerCase() === 'tender';
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 (isTenderUser) {
return (
<View style={styles.container}>
<InsideHeader title="Auction" showArrow />
<View style={styles.restrictedContainer}>
<View style={styles.restrictedCard}>
{/* Icon ring */}
<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 a{' '}
<AppText variant="body" color={colors.primary} style={{ fontWeight: '700' } as any}>
Tender user
</AppText>
{'. '}Auction features are not available for your account type.
</AppText>
<View style={styles.hintBox}>
<AppText variant="bodySmall" centered color={colors.textSecondary}>
Switch to an Auction account to access live & purchase auction 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 Auction Login
</AppText>
</TouchableOpacity>
</View>
</View>
<Text style={styles.restrictedTitle}>Auction Access Restricted</Text>
<Text style={styles.restrictedMessage}>
You are currently logged in as a Tender user. Auction features are not available for your account type.
</Text>
<Text style={styles.restrictedSubMessage}>
To access auction features, please login with an Auction user account.
</Text>
<TouchableOpacity
style={styles.loginButton}
onPress={() => Navigation.navigate(NavigationString.Login)}
>
<Text style={styles.loginButtonText}>Switch to Auction Login</Text>
</TouchableOpacity>
</View>
</View>
);
);
}
return (
<View style={styles.container}>
<InsideHeader title="Auction" showArrow />
{isTenderUser ? (
// Show restricted access message for tender users
renderRestrictedAccess()
) : (
// Show normal auction UI for auction 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' ? <LiveAuction /> : <PurchaseAuction />}
<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.PurchaseList);
}}
>
<ShoppingCart size={22} color={colors.black} strokeWidth={2} />
</TouchableOpacity>
</View>
{/* Cart Floating Button */}
<TouchableOpacity
style={styles.cartFloatingButton}
activeOpacity={0.9}
onPress={() => Navigation.navigate(NavigationString.PurchaseList)}
>
<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' ? <LiveAuction /> : <PurchaseAuction />}
</>
)}
)}
</TouchableOpacity>
</View>
);
};
export default Auction;
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: 13,
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%',
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any, TAB_WIDTH: number, TAB_HEIGHT: number) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
},
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,
},
});
/* ── 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,
});
+189 -144
View File
@@ -1,162 +1,207 @@
import React, { useCallback, useState, useEffect, useRef } from 'react';
import { View, FlatList, ActivityIndicator, RefreshControl, Text } from 'react-native';
import {
View,
FlatList,
ActivityIndicator,
RefreshControl,
StyleSheet,
} from 'react-native';
import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated';
import { RefreshCw } from 'lucide-react-native';
import LiveAuctionTile from './LiveAuctionTile';
import { useFetchLiveAuction } from '../../Services/ApiCall/Hooks';
import { SearchInput } from '../../Component/Serach/SeacrhInput';
import { Skelton } from '../../Component/Index';
import colors from '../../Constant/Color';
import SkeltonList from '../../Component/Skelton/SkeltonList';
import { useDispatch, useSelector } from 'react-redux';
import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
import { apiCall } from '../../Axios/Axios';
import { GET_MY_AUCTION } from '../../Services/ApiCall/ApiUrls';
import SkeltonList from '../../Component/Skelton/SkeltonList';
import Skeleton from '../../Component/Skelton/Skelton';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const LiveAuction = () => {
const Dispatch = useDispatch()
const user = useSelector((state: any) => state.token?.token);
const onEndReachedCalledDuringMomentum = useRef(true);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const [showRefreshBanner, setShowRefreshBanner] = useState(false);
const [allBoolean, setAllBoolean] = useState({ liveAuctionData: [], isLoading: false })
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch, isPending }: any = useFetchLiveAuction();
// My Methods
const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const user = useSelector((state: any) => state.token?.token);
const onEndReachedCalledDuringMomentum = useRef(true);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const [showRefreshBanner, setShowRefreshBanner] = useState(false);
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isRefetching,
refetch,
isPending,
}: any = useFetchLiveAuction();
const renderItem = useCallback(
({ item, index }: any) => {
const isPurchase = SelectedPurchaseList.findIndex((prev: any) => prev.auctionNumber === item.auctionNumber)
return <LiveAuctionTile
data={item}
index={index}
isPurchasedByContractor={item.contractorId?.includes(String(user.contractorId))}
auctionId={item.auctionId}
auctionDispatchDate={item.auctionDispatchDate}
isPurchased={isPurchase !== -1 ? true : false}
purchaseEnd={item.purchaseEnd}
auctionOfWork={item.auctionOfWork}
auctionFees={item.auctionFees}
auctionNumber={item.auctionNumber}
onPurchasePress={() => { Dispatch(togglePurchaseAuction(item)) }}
onDetailsPress={() => { }}
/>
},
[SelectedPurchaseList]
);
const renderItem = useCallback(
({ item, index }: any) => {
const isPurchase = SelectedPurchaseList.findIndex(
(prev: any) => prev.auctionNumber === item.auctionNumber,
);
return (
<LiveAuctionTile
data={item}
index={index}
isPurchasedByContractor={item.contractorId?.includes(
String(user.contractorId),
)}
auctionId={item.auctionId}
auctionDispatchDate={item.auctionDispatchDate}
isPurchased={isPurchase !== -1}
purchaseEnd={item.purchaseEnd}
auctionOfWork={item.auctionOfWork}
auctionFees={item.auctionFees}
auctionNumber={item.auctionNumber}
onPurchasePress={() => {
console.log('[LiveAuction] purchase toggled:', item?.auctionNumber);
Dispatch(togglePurchaseAuction(item));
}}
onDetailsPress={() => {}}
/>
);
},
[SelectedPurchaseList],
);
const loadMore = useCallback(() => {
if (onEndReachedCalledDuringMomentum.current) return;
if (!hasNextPage || isFetchingNextPage) return;
fetchNextPage();
onEndReachedCalledDuringMomentum.current = true;
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
const renderFooter = useCallback(() => {
if (!isFetchingNextPage) return null;
return (
<View style={{ paddingVertical: 12 }}>
<ActivityIndicator size="small" color={colors.primary} />
</View>
);
}, [isFetchingNextPage]);
useEffect(() => {
if (isRefetching) {
setShowRefreshBanner(true);
} else {
const timer = setTimeout(() => {
setShowRefreshBanner(false);
}, 1000);
return () => clearTimeout(timer);
}
}, [isRefetching]);
if (isLoading) {
return <Skelton />;
}
const loadMore = useCallback(() => {
if (onEndReachedCalledDuringMomentum.current) return;
if (!hasNextPage || isFetchingNextPage) return;
console.log('[LiveAuction] loadMore: fetching next page');
fetchNextPage();
onEndReachedCalledDuringMomentum.current = true;
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
const renderFooter = useCallback(() => {
if (!isFetchingNextPage) return null;
return (
<View style={{ flex: 1 }}>
<SearchInput onChange={() => { }} placeholder="Search...." filter={false} />
{showRefreshBanner && (
<Animated.View
entering={FadeInDown.duration(200)}
exiting={FadeOutUp.duration(500)}
style={{
alignSelf: 'center',
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingVertical: 8,
paddingHorizontal: 14,
marginTop: 6,
borderRadius: 20,
backgroundColor: '#FFFFFF',
shadowColor: '#000',
shadowOpacity: 0.06,
shadowRadius: 8,
shadowOffset: { width: 0, height: 3 },
elevation: 4,
zIndex: 10, // Ensure it stays on top
position: 'absolute',
top: 60, // Adjust based on your SearchInput height
}}
>
<ActivityIndicator size="small" color={colors.primary} />
<Text
style={{
fontSize: 13,
color: '#374151',
fontWeight: '500',
}}
>
Refreshing auctions
</Text>
</Animated.View>
)}
{
isPending ? <Skeleton /> :
<FlatList
data={data?.result ?? []}
renderItem={renderItem}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 50, paddingTop: 10 }}
onEndReached={loadMore}
onEndReachedThreshold={0.1}
onMomentumScrollBegin={() => {
onEndReachedCalledDuringMomentum.current = false;
}}
ListFooterComponent={renderFooter}
refreshControl={
<RefreshControl
refreshing={isRefetching}
onRefresh={refetch}
tintColor="transparent"
colors={[colors.primary]}
/>
}
initialNumToRender={6}
maxToRenderPerBatch={6}
windowSize={7}
removeClippedSubviews
/>
}
</View>
<View style={styles.footerLoader}>
<ActivityIndicator size="small" color={colors.primary} />
<AppText variant="caption" color={colors.textSecondary}>
Loading more…
</AppText>
</View>
);
}, [isFetchingNextPage, colors]);
useEffect(() => {
if (isRefetching) {
console.log('[LiveAuction] refetch started');
setShowRefreshBanner(true);
} else {
console.log('[LiveAuction] refetch completed');
const timer = setTimeout(() => setShowRefreshBanner(false), 1000);
return () => clearTimeout(timer);
}
}, [isRefetching]);
useEffect(() => {
console.log('[LiveAuction] data updated:', {
count: data?.result?.length ?? 0,
hasNextPage,
isFetchingNextPage,
isPending,
});
}, [data, hasNextPage, isFetchingNextPage, isPending]);
if (isLoading) {
return <Skelton />;
}
return (
<View style={styles.root}>
{/* Search */}
<SearchInput onChange={() => {}} placeholder="Search auctions…" filter={false} />
{/* Refresh Banner */}
{showRefreshBanner && (
<Animated.View
entering={FadeInDown.duration(200)}
exiting={FadeOutUp.duration(400)}
style={[
styles.refreshBanner,
{
backgroundColor: colors.surface,
...shadows.medium,
},
]}
>
<ActivityIndicator size="small" color={colors.primary} />
<AppText variant="caption" color={colors.text} style={styles.refreshText as any}>
Refreshing auctions…
</AppText>
</Animated.View>
)}
{/* List */}
{isPending ? (
<SkeltonList />
) : (
<FlatList
data={data?.result ?? []}
renderItem={renderItem}
keyExtractor={(item: any) => item?.auctionId?.toString()}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContent}
onEndReached={loadMore}
onEndReachedThreshold={0.1}
onMomentumScrollBegin={() => {
onEndReachedCalledDuringMomentum.current = false;
}}
ListFooterComponent={renderFooter}
refreshControl={
<RefreshControl
refreshing={isRefetching}
onRefresh={refetch}
tintColor="transparent"
colors={[colors.primary]}
/>
}
initialNumToRender={6}
maxToRenderPerBatch={6}
windowSize={7}
removeClippedSubviews
/>
)}
</View>
);
};
export default LiveAuction;
export default LiveAuction;
const styles = StyleSheet.create({
root: {
flex: 1,
},
listContent: {
paddingHorizontal: 16,
paddingBottom: 60,
paddingTop: 10,
},
refreshBanner: {
alignSelf: 'center',
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingVertical: 8,
paddingHorizontal: 16,
marginTop: 6,
borderRadius: 20,
zIndex: 10,
position: 'absolute',
top: 60,
},
refreshText: {
fontWeight: '500',
} as any,
footerLoader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
paddingVertical: 16,
},
});
+252 -218
View File
@@ -1,13 +1,26 @@
import React, { memo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import Animated, { FadeInDown } from 'react-native-reanimated';
import moment from 'moment';
import colors from '../../Constant/Color';
import { LiveAuctionTileProps } from './types/types';
import AuctionDetail from './Modal/AuctionDetail';
import { Calendar, Tag, Info, ArrowRight } from 'lucide-react-native';
import {
Tag,
Calendar,
Clock,
ShoppingCart,
Trash2,
CheckCircle2,
XCircle,
} from 'lucide-react-native';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import NavigationString from '../../Constant/NavigationString';
import { useNavigation } from '@react-navigation/native';
const LiveAuctionTile: React.FC<LiveAuctionTileProps> = ({
const LiveAuctionTile: React.FC<LiveAuctionTileProps & { index?: number }> = ({
data,
index = 0,
isPurchased = false,
onPurchasePress,
auctionDispatchDate,
@@ -15,231 +28,252 @@ const LiveAuctionTile: React.FC<LiveAuctionTileProps> = ({
purchaseEnd,
auctionFees,
auctionNumber,
isPurchasedByContractor
isPurchasedByContractor,
}) => {
const { colors, spacing, shadows } = useTheme();
const [showDetail, setShowDetail] = React.useState(false);
const purchaseEnded = moment().isAfter(moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss'));
const deadlineFormatted = moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, YY');
const Navigation = useNavigation() as any;
const getStatus = () => {
if (isPurchasedByContractor) return 'purchased';
if (purchaseEnded) return 'expired';
if (isPurchased) return 'in_cart';
return 'active';
};
const status = getStatus();
const styles = makeStyles(colors, spacing, shadows);
if (showDetail) {
return (
<View style={styles.card}>
<AuctionDetail visible={showDetail} data={data} onClose={() => setShowDetail(false)} />
</View>
);
}
return (
<View style={styles.cardWrapper}>
{
!showDetail ?
<>
<View
style={[
styles.statusTag,
{ backgroundColor: isPurchased ? colors.error : purchaseEnded ? '#94a3b8' : '#4ade80' },
]}
<Animated.View
entering={FadeInDown.delay(index * 60).springify()}
style={styles.card}
>
{/* ── Row 1: Number + Fee ── */}
<View style={styles.topRow}>
<View style={styles.numberRow}>
<Tag size={14} color={colors.primary} strokeWidth={2} />
<AppText variant="bodySmall" color={colors.primary} style={styles.auctionNo as any}>
#{auctionNumber}
</AppText>
</View>
<AppText variant="h4" color={colors.primary} style={styles.fee as any}>
₹{auctionFees}
</AppText>
</View>
{/* ── Row 2: Title ── */}
<AppText variant="h4" color={colors.text} numberOfLines={2} style={styles.title as any}>
{auctionOfWork ?? 'General Auction Work'}
</AppText>
{/* ── Row 3: Date strip ── */}
<View style={[styles.dateStrip, { backgroundColor: colors.overlay }]}>
{/* Start */}
<View style={styles.dateCol}>
<View style={styles.dateHeader}>
<Calendar size={13} color={colors.textSecondary} strokeWidth={2} />
<AppText variant="caption" color={colors.textSecondary} style={styles.dateLabel as any}>
Start Date
</AppText>
</View>
<AppText variant="caption" color={colors.text} style={styles.dateValue as any}>
{auctionDispatchDate ?? '—'}
</AppText>
</View>
{/* Separator */}
<View style={[styles.separator, { backgroundColor: colors.border }]} />
{/* Deadline */}
<View style={styles.dateCol}>
<View style={styles.dateHeader}>
<Clock
size={13}
color={purchaseEnded ? colors.error : colors.success}
strokeWidth={2}
/>
<View
style={styles.container}
>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity style={styles.badge} onPress={() => setShowDetail(true)}>
<Tag size={14} color={colors.primary} />
<Text style={styles.badgeText}>#{auctionNumber}</Text>
</TouchableOpacity>
<Text style={styles.priceText}>₹{auctionFees}</Text>
</View>
<AppText variant="caption" color={colors.textSecondary} style={styles.dateLabel as any}>
Deadline
</AppText>
</View>
<AppText
variant="caption"
color={purchaseEnded ? colors.error : colors.text}
style={styles.dateValue as any}
>
{deadlineFormatted}
</AppText>
</View>
</View>
{/* Work Title */}
<Text style={styles.workTitle} numberOfLines={2}>
{auctionOfWork ?? 'General Auction Work'}
</Text>
{/* ── Row 4: Actions ── */}
<View style={styles.actionRow}>
{/* Ghost Details button */}
<TouchableOpacity
style={[styles.detailsBtn, { borderColor: colors.border }]}
onPress={() => setShowDetail(true)}
activeOpacity={0.7}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.detailsText as any}>
View Details
</AppText>
</TouchableOpacity>
{/* 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}>{auctionDispatchDate ?? '-'}</Text>
</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, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, YY')}
</Text>
</View>
</View>
</View>
{/* Action Bar */}
<View style={styles.actionBar}>
<TouchableOpacity style={styles.iconBtn} onPress={() => setShowDetail(true)}>
<Info size={18} color={colors.primary} />
<Text style={styles.iconBtnText}>Details</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.mainButton,
purchaseEnded
? styles.btnDisabled
: isPurchasedByContractor ? { backgroundColor: 'lightgray' } : isPurchased
? styles.btnPurchased
: styles.btnActive,
]}
onPress={onPurchasePress}
disabled={purchaseEnded ? true : !isPurchasedByContractor ? false : true}
>
<Text
style={[
styles.mainButtonText,
isPurchased && !purchaseEnded ? { color: colors.white } : { color: '#fff' },
]}
>
{purchaseEnded ? (isPurchasedByContractor ? 'Purchased' : 'Expired') : isPurchasedByContractor ? 'Purchased' : isPurchased ? 'Remove' : 'Add to Cart'}
</Text>
{!purchaseEnded && !isPurchasedByContractor && (
<ArrowRight size={16} color={isPurchased ? colors.white : '#fff'} />
)}
</TouchableOpacity>
</View>
{/* Auction Detail Modal */}
</View>
</>
: <AuctionDetail visible={showDetail} data={data} onClose={() => setShowDetail(false)} />
}
</View>
{/* CTA */}
{status === 'active' && (
<TouchableOpacity
style={[styles.ctaBtn, { backgroundColor: colors.primary }]}
onPress={onPurchasePress}
activeOpacity={0.85}
>
<ShoppingCart size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
ADD TO CART
</AppText>
</TouchableOpacity>
)}
{status === 'in_cart' && (
<TouchableOpacity
style={[styles.ctaBtn, { backgroundColor: colors.success }]}
onPress={() => { Navigation.navigate(NavigationString.PurchaseList) }}
activeOpacity={0.85}
>
<ShoppingCart size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
GO TO CART
</AppText>
</TouchableOpacity>
)}
{status === 'purchased' && (
<View style={[styles.ctaBtn, { backgroundColor: colors.secondary }]}>
<CheckCircle2 size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
PURCHASED
</AppText>
</View>
)}
{status === 'expired' && (
<View style={[styles.ctaBtn, { backgroundColor: colors.secondary }]}>
<XCircle size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
EXPIRED
</AppText>
</View>
)}
</View>
</Animated.View>
);
};
export default memo(LiveAuctionTile);
const styles = StyleSheet.create({
cardWrapper: {
marginVertical: 10,
marginHorizontal: 16,
backgroundColor: '#fff',
borderRadius: 24,
flexDirection: 'row',
overflow: 'hidden',
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
card: {
backgroundColor: colors.surface,
borderRadius: 18,
borderWidth: 1,
borderColor: colors.border,
marginVertical: 7,
padding: spacing.m,
...shadows.medium,
},
// Modern shadow
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.08,
shadowRadius: 20,
elevation: 8,
},
statusTag: {
width: 6,
height: '100%',
},
container: {
flex: 1,
padding: 20,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
badge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primary + '25',
paddingHorizontal: 12,
paddingVertical: 5,
borderRadius: 12,
gap: 6,
},
badgeText: {
fontSize: 12,
fontWeight: '700',
color: colors.primary,
},
priceText: {
fontSize: 20,
fontWeight: '800',
color: '#0f172a',
},
workTitle: {
fontSize: 16,
fontWeight: '600',
color: '#334155',
lineHeight: 24,
marginBottom: 16,
},
infoGrid: {
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: '#f0f4f8',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 16,
marginBottom: 18,
},
infoItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
infoTextGroup: {
flexDirection: 'column',
},
infoLabel: {
fontSize: 10,
color: '#94a3b8',
textTransform: 'uppercase',
fontWeight: '700',
},
infoValue: {
fontSize: 13,
fontWeight: '700',
color: '#1e293b',
},
actionBar: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 12,
},
iconBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 10,
backgroundColor: '#e2e8f0',
},
iconBtnText: {
fontSize: 14,
fontWeight: '700',
color: colors.primary,
},
mainButton: {
flex: 1,
height: 46,
borderRadius: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
},
btnActive: {
backgroundColor: '#4ade80', // light green
},
btnPurchased: {
backgroundColor: colors.error,
borderWidth: 2,
borderColor: colors.error,
},
btnDisabled: {
backgroundColor: '#e2e8f0',
},
mainButtonText: {
fontSize: 14,
fontWeight: '800',
},
});
/* Top row */
topRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.s,
},
numberRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
},
auctionNo: {
fontWeight: '700',
} as any,
fee: {
fontWeight: '800',
} as any,
/* Title */
title: {
fontWeight: '700',
lineHeight: 24,
marginBottom: spacing.m,
} as any,
/* Date strip */
dateStrip: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 12,
paddingVertical: spacing.s + 2,
paddingHorizontal: spacing.m,
marginBottom: spacing.m,
},
dateCol: {
flex: 1,
},
dateHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
marginBottom: 3,
},
dateLabel: {
fontWeight: '500',
} as any,
dateValue: {
fontWeight: '700',
} as any,
separator: {
width: StyleSheet.hairlineWidth,
height: 36,
marginHorizontal: spacing.m,
},
/* Actions */
actionRow: {
flexDirection: 'row',
gap: spacing.s,
alignItems: 'center',
},
detailsBtn: {
flex: 1,
paddingVertical: 10,
borderWidth: 1,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
detailsText: {
fontWeight: '600',
} as any,
ctaBtn: {
flex: 2,
height: 44,
borderRadius: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
},
ctaText: {
fontWeight: '800',
letterSpacing: 0.8,
} as any,
});
+330 -303
View File
@@ -1,64 +1,66 @@
import React, { memo, useEffect, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform, StatusBar, Modal } from 'react-native';
import { Briefcase, Calendar, FileText, Download, Eye, Layers, ArrowLeft, Clock } from 'lucide-react-native';
import {
View,
StyleSheet,
TouchableOpacity,
ScrollView,
Modal,
ActivityIndicator,
} from 'react-native';
import {
Briefcase,
Calendar,
FileText,
Download,
Eye,
Layers,
ArrowLeft,
Clock,
IndianRupee,
Wrench,
Mail,
} from 'lucide-react-native';
import { useAuctionItem, useGetAuciton, useGetAucitonDocument } from '../../../Services/ApiCall/Hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { AUCTION_DOCUMENT_DOWNLOAD } from '../../../Services/ApiCall/ApiUrls';
import ReactNativeBlobUtil from 'react-native-blob-util';
import AuctionItemDetail from './AuctionItemDetail';
import { Loader } from '../../../Component/Index';
import colors from '../../../Constant/Color';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import moment from 'moment';
interface Props { visible: boolean; onClose: () => void; data?: any }
const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const inset = useSafeAreaInsets();
const { colors, spacing, shadows } = useTheme();
const inset = useSafeAreaInsets()
const { data: auctionData, isLoading } = useGetAuciton({ auctionId: data?.auctionId })
const [aucitonDetail, setAuciotnDetail] = useState<any>(auctionData)
const [allBoolean, setAllBoolean] = useState({ showItemDetail: false, selectedItem: {}, fileLoading: false });
const { data: auctionDocuments } = useGetAucitonDocument({ auctionId: data?.auctionId })
const { mutate } = useAuctionItem()
const { data: auctionData } = useGetAuciton({ auctionId: data?.auctionId });
const [aucitonDetail, setAuciotnDetail] = useState<any>(auctionData);
const [allBoolean, setAllBoolean] = useState({
showItemDetail: false,
selectedItem: {},
fileLoading: false,
});
const { data: auctionDocuments } = useGetAucitonDocument({ auctionId: data?.auctionId });
const { mutate } = useAuctionItem();
const formatDate = (date?: string) => {
if (!date) return '-';
if (!date) return '—';
return moment(date, 'DD-MM-YYYY HH:mm:ss').format('DD MMM YYYY, hh:mm A');
};
const isLive = data?.purchaseEnd ? moment().isBefore(moment(data.purchaseEnd, 'DD-MM-YYYY HH:mm:ss')) : false;
const Section = ({ title, icon: Icon, children }: any) => (
<View style={styles.sectionContainer}>
<View style={styles.sectionHeader}>
<Icon size={20} color={colors.primary} />
<Text style={styles.sectionTitle}>{title}</Text>
</View>
{children}
</View>
);
const InfoBox = ({ label, value, full = false }: any) => (
<View style={[styles.infoBox, full ? { width: '100%' } : { width: '48%' }]}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.value}>{value || '-'}</Text>
</View>
);
const isLive = data?.purchaseEnd
? moment().isBefore(moment(data.purchaseEnd, 'DD-MM-YYYY HH:mm:ss'))
: false;
const handleDownload = async (id: number, fileName: string) => {
try {
setAllBoolean((prev: any) => ({ ...prev, fileLoading: true }));
setAllBoolean(prev => ({ ...prev, fileLoading: true }));
const { dirs } = ReactNativeBlobUtil.fs;
const path = `${dirs.DownloadDir}/${fileName}.pdf`;
const downloadUrl = `${AUCTION_DOCUMENT_DOWNLOAD}?documentId=${id}&isAmendment=false`;
const response = await ReactNativeBlobUtil.config({
await ReactNativeBlobUtil.config({
fileCache: false,
addAndroidDownloads: {
useDownloadManager: true,
@@ -69,315 +71,340 @@ const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
description: 'Downloading document',
mediaScannable: true,
},
}).fetch('GET', downloadUrl, {
Accept: 'application/pdf',
});
const stat = await ReactNativeBlobUtil.fs.stat(path);
}).fetch('GET', downloadUrl, { Accept: 'application/pdf' });
} catch (error: any) {
console.log('Error:', error?.message || error);
} finally {
setAllBoolean((prev: any) => ({ ...prev, fileLoading: false }));
setAllBoolean(prev => ({ ...prev, fileLoading: false }));
}
};
useEffect(() => {
mutate({ auctionNumber: data?.auctionNumber, status: data?.auctionPattern }, {
onSuccess: (res) => {
setAuciotnDetail((prev: any) => ({ ...prev, auctionItems: res.data, auctionDocuments }))
}, onError: (err) => {
console.log(err, '-----auction item error');
}
});
mutate(
{ auctionNumber: data?.auctionNumber, status: data?.auctionPattern },
{
onSuccess: (res: any) => {
setAuciotnDetail((prev: any) => ({ ...prev, auctionItems: res.data, auctionDocuments }));
},
onError: (err: any) => {
console.log(err, '-----auction item error');
},
},
);
}, [data.auctionNumber, auctionDocuments]);
const isAmended = auctionData?.auctionStatus === 'Amended';
const kd = (key: any, amended: any) => isAmended ? amended : key;
const styles = makeStyles(colors, spacing, shadows);
return (
<Modal
visible={visible}
transparent={false}
animationType="none"
animationType="slide"
statusBarTranslucent
onRequestClose={onClose}
>
<View style={styles.safeArea}>
<StatusBar barStyle="dark-content" />
{/* Full Header */}
<View style={[styles.header, { paddingTop: inset.top }]}>
<TouchableOpacity onPress={onClose} style={styles.backBtn}>
<ArrowLeft size={24} color="#1e293b" />
<View style={styles.root}>
{/* ── Header ── */}
<View style={[styles.header, { paddingTop: inset.top + 6 }]}>
<TouchableOpacity onPress={onClose} style={styles.backBtn} activeOpacity={0.7}>
<ArrowLeft size={22} color={colors.text} strokeWidth={2.5} />
</TouchableOpacity>
<View style={styles.headerTitleContainer}>
<Text style={styles.headerTitleText}>Auction Overview</Text>
<Text style={styles.headerIdText}>Ref: {data?.auctionNumber || 'N/A'}</Text>
<View style={styles.headerCenter}>
<AppText variant="h4" color={colors.text} style={styles.headerTitle as any}>
Auction Overview
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
Ref: {data?.auctionNumber ?? 'N/A'}
</AppText>
</View>
<View style={styles.liveIndicator}>
<View style={[styles.dot, { backgroundColor: isLive ? '#22c55e' : '#94a3b8' }]} />
<Text style={styles.liveText}>{isLive ? 'LIVE' : 'CLOSED'}</Text>
<View style={[styles.statusChip, { borderColor: isLive ? `${colors.success}50` : colors.border }]}>
<View style={[styles.statusDot, { backgroundColor: isLive ? colors.success : colors.textSecondary }]} />
<AppText variant="caption" color={isLive ? colors.success : colors.textSecondary} style={styles.statusText as any}>
{isLive ? 'LIVE' : 'CLOSED'}
</AppText>
</View>
</View>
{/* Scrollable Body */}
{/* ── Body ── */}
<ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={true}
style={{ backgroundColor: colors.background }}
>
{/* 1. Basic Info Grid */}
<Section title="Details" icon={Layers}>
<View style={styles.grid}>
<InfoBox label="Auction No." value={data?.auctionNumber} />
<InfoBox label="Pattern" value={data?.auctionPattern || 'Forward'} />
<InfoBox label="Mode" value={data?.auctionMode || 'Open'} />
<InfoBox label="Category" value={data?.categoryOfAuction || 'General'} />
<InfoBox label="EMD Type" value={data?.emd || 'Online'} />
<InfoBox label="EMD Figure" value={data?.emdFigure || 'Item Wise'} />
</View>
</Section>
{/* 2. Work Description */}
<Section title="Auction Details" icon={Briefcase}>
<View style={styles.grid}>
<InfoBox label="Auction Name" value={auctionData?.auctionOfWork} />
<InfoBox label="Category Of Auction " value={auctionData?.auctionCategories} />
<InfoBox label="Auction Fees (In Figure) " value={auctionData?.auctionFees} />
<InfoBox label="Auction Fees (In Words)" value={auctionData?.auctionFeesInWord} />
<InfoBox label="Auction Keyword" value={auctionData?.auctionKeywordName} />
</View>
</Section>
{/* BASIC DETAILS */}
<SectionCard title="Basic Details" icon={Layers} colors={colors} spacing={spacing} shadows={shadows}>
<InfoRow label="Auction No." value={data?.auctionNumber} colors={colors} />
<InfoRow label="Pattern" value={data?.auctionPattern || 'Forward'} colors={colors} />
<InfoRow label="Mode" value={data?.auctionMode || 'Open'} colors={colors} />
<InfoRow label="Category" value={data?.categoryOfAuction || 'General'} colors={colors} />
<InfoRow label="EMD Type" value={data?.emd || 'Online'} colors={colors} last />
</SectionCard>
{/* 3. Timeline Section */}
<Section title="Key Dates" icon={Calendar}>
<View style={styles.timelineContainer}>
<View style={styles.timelineRow}>
<View style={styles.iconCircle}><Clock size={16} color="#64748b" /></View>
<View>
<Text style={styles.label}>Auction Purchase (Start)</Text>
<Text style={styles.value}>{formatDate(
auctionData?.auctionStatus === "Amended" ? auctionData?.amendedkeyDates.auctionPurchase.startDateTime : auctionData?.keyDates.auctionPurchase.startDateTime)}</Text>
</View>
</View>
<View style={styles.timelineConnector} />
<View style={styles.timelineRow}>
<View style={[styles.iconCircle, { backgroundColor: '#fee2e2' }]}><Clock size={16} color="#ef4444" /></View>
<View>
<Text style={styles.label}>Purchase Purchase (End)</Text>
<Text style={styles.value}>{formatDate(
auctionData?.auctionStatus === "Amended" ? auctionData?.amendedkeyDates.auctionPurchase.endDateTime : auctionData?.keyDates.auctionPurchase.endDateTime)}</Text>
</View>
</View>
<View style={styles.timelineConnector} />
<View style={styles.timelineRow}>
<View style={styles.iconCircle}><Clock size={16} color="#64748b" /></View>
<View>
<Text style={styles.label}>Bidding Start (Start)</Text>
<Text style={styles.value}>{formatDate(
auctionData?.auctionStatus === "Amended" ? auctionData?.amendedkeyDates.auctionBidding.startDateTime : auctionData?.keyDates.auctionBidding.startDateTime)}</Text>
</View>
</View>
<View style={styles.timelineConnector} />
<View style={styles.timelineRow}>
<View style={[styles.iconCircle, { backgroundColor: '#fee2e2' }]}><Clock size={16} color="#ef4444" /></View>
<View>
<Text style={styles.label}>Bidding Deadline (End)</Text>
<Text style={styles.value}>{formatDate(
auctionData?.auctionStatus === "Amended" ? auctionData?.amendedkeyDates.auctionBidding.endDateTime : auctionData?.keyDates.auctionBidding.endDateTime)}</Text>
</View>
</View>
</View>
</Section>
{/* AUCTION DETAILS */}
<SectionCard title="Auction Details" icon={Briefcase} colors={colors} spacing={spacing} shadows={shadows}>
<InfoRow label="Name" value={auctionData?.auctionOfWork} colors={colors} multiline />
<InfoRow label="Category" value={auctionData?.auctionCategories} colors={colors} />
<InfoRow label="Fees" value={auctionData?.auctionFees ? `₹ ${auctionData.auctionFees}` : '—'} colors={colors} valueColor={colors.primary} />
<InfoRow label="Fees (Words)" value={auctionData?.auctionFeesInWord} colors={colors} multiline />
<InfoRow label="Keyword" value={auctionData?.auctionKeywordName} colors={colors} last />
</SectionCard>
{/* 4. Item Table Section */}
<Section title="Auction Items" icon={FileText}>
<View style={styles.tableWrapper}>
<View style={styles.tableHead}>
<Text style={[styles.tableLabel, { flex: 2 }]}>Item</Text>
<Text style={styles.tableLabel}>Time</Text>
<Text style={[styles.tableLabel, { textAlign: 'right' }]}>View</Text>
</View>
{/* Dummy Item Row */}
{aucitonDetail?.auctionItems?.map((item: any, index: number) => (
<View style={styles.tableRow}>
<View style={{ flex: 2 }}>
<Text style={styles.itemMain}>{item.productName}</Text>
<Text style={styles.itemSub}>Category: {item?.categoryName}</Text>
{/* KEY DATES */}
<SectionCard title="Key Dates" icon={Calendar} colors={colors} spacing={spacing} shadows={shadows}>
<InfoRow label="Purchase Start" value={formatDate(kd(auctionData?.keyDates?.auctionPurchase?.startDateTime, auctionData?.amendedkeyDates?.auctionPurchase?.startDateTime))} colors={colors} />
<InfoRow label="Purchase End" value={formatDate(kd(auctionData?.keyDates?.auctionPurchase?.endDateTime, auctionData?.amendedkeyDates?.auctionPurchase?.endDateTime))} colors={colors} valueColor={colors.error} />
<InfoRow label="Bidding Start" value={formatDate(kd(auctionData?.keyDates?.auctionBidding?.startDateTime, auctionData?.amendedkeyDates?.auctionBidding?.startDateTime))} colors={colors} />
<InfoRow label="Bidding End" value={formatDate(kd(auctionData?.keyDates?.auctionBidding?.endDateTime, auctionData?.amendedkeyDates?.auctionBidding?.endDateTime))} colors={colors} valueColor={colors.error} last />
</SectionCard>
{/* AUCTION ITEMS */}
{aucitonDetail?.auctionItems?.length > 0 && (
<SectionCard title="Auction Items" icon={FileText} colors={colors} spacing={spacing} shadows={shadows}>
{aucitonDetail.auctionItems.map((item: any, index: number) => (
<View
key={index}
style={[
styles.itemRow,
{ borderBottomColor: colors.border },
index === aucitonDetail.auctionItems.length - 1 && { borderBottomWidth: 0 },
]}
>
<View style={styles.itemLeft}>
<AppText variant="bodySmall" color={colors.text} style={styles.itemName as any}>
{item.productName}
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
{item?.categoryName} · {item.timeDurationType}
</AppText>
</View>
<Text style={styles.itemQty}>{item.timeDurationType}</Text>
<TouchableOpacity style={styles.actionIcon} onPress={() => { setAllBoolean((prev: any) => ({ ...prev, showItemDetail: true, selectedItem: item })) }}>
<Eye size={18} color={colors.primary} />
<TouchableOpacity
style={[styles.eyeBtn, { backgroundColor: `${colors.primary}15` }]}
onPress={() => setAllBoolean(prev => ({ ...prev, showItemDetail: true, selectedItem: item }))}
>
<Eye size={16} color={colors.primary} strokeWidth={2} />
</TouchableOpacity>
</View>
))}
</View>
</Section>
</SectionCard>
)}
{/* 5. Downloads */}
<Section title="Downloads" icon={Download}>
{aucitonDetail?.auctionDocuments?.map((doc: any, index: number) => (
<TouchableOpacity style={styles.fileCard} onPress={() => {
handleDownload(
doc.auctionDocumentId,
doc.fileName.replace('.pdf', '')
)
}}>
<View style={styles.fileIcon}><FileText size={20} color="#ef4444" /></View>
<View style={{ flex: 1 }}>
<Text style={styles.fileName}>{doc.fileName}</Text>
</View>
{allBoolean.fileLoading ? (
<View >
<Loader size='small' color='#94a3b8' />
{/* DOCUMENTS */}
{aucitonDetail?.auctionDocuments?.length > 0 && (
<SectionCard title="Documents" icon={Download} colors={colors} spacing={spacing} shadows={shadows}>
{aucitonDetail.auctionDocuments.map((doc: any, index: number) => (
<TouchableOpacity
key={index}
style={[
styles.docRow,
{ borderBottomColor: colors.border },
index === aucitonDetail.auctionDocuments.length - 1 && { borderBottomWidth: 0 },
]}
activeOpacity={0.7}
onPress={() => handleDownload(doc.auctionDocumentId, doc.fileName.replace('.pdf', ''))}
>
<View style={[styles.docIcon, { backgroundColor: `${colors.error}15` }]}>
<FileText size={16} color={colors.error} strokeWidth={2} />
</View>
) : (
<Download size={20} color="#94a3b8" />
)}
</TouchableOpacity>
))}
</Section>
<AppText variant="bodySmall" color={colors.text} style={styles.docName as any} numberOfLines={1}>
{doc.fileName}
</AppText>
{allBoolean.fileLoading
? <ActivityIndicator size="small" color={colors.primary} />
: <Download size={18} color={colors.textSecondary} strokeWidth={2} />
}
</TouchableOpacity>
))}
</SectionCard>
)}
</ScrollView>
{allBoolean.showItemDetail && <AuctionItemDetail data={allBoolean.selectedItem} onClose={() => { setAllBoolean((prev: any) => ({ ...prev, showItemDetail: false })) }} visible={allBoolean.showItemDetail} />}
{allBoolean.showItemDetail && (
<AuctionItemDetail
data={allBoolean.selectedItem}
onClose={() => setAllBoolean(prev => ({ ...prev, showItemDetail: false }))}
visible={allBoolean.showItemDetail}
/>
)}
</View>
</Modal >
</Modal>
);
};
const styles = StyleSheet.create({
fullModal: {
margin: 0,
backgroundColor: '#fff',
},
safeArea: {
flex: 1,
backgroundColor: '#fff',
},
header: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#f1f5f9',
backgroundColor: '#fff',
},
backBtn: {
padding: 8,
marginRight: 8,
},
headerTitleContainer: {
flex: 1,
},
headerTitleText: {
fontSize: 18,
fontWeight: '800',
color: '#0f172a',
},
headerIdText: {
fontSize: 13,
color: '#64748b',
},
liveIndicator: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f8fafc',
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
gap: 6,
borderWidth: 1,
borderColor: '#e2e8f0',
},
dot: { width: 8, height: 8, borderRadius: 4 },
liveText: { fontSize: 11, fontWeight: '800', color: '#475569' },
export default memo(AuctionFullDetail);
scrollView: { flex: 1, backgroundColor: '#f8fafc' },
scrollContent: { padding: 16, paddingBottom: 30 },
/* ─── Sub-components ─── */
sectionContainer: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: '#eef2f6',
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
marginBottom: 20,
paddingBottom: 10,
borderBottomWidth: 1,
borderBottomColor: '#f8fafc',
},
sectionTitle: {
fontSize: 14,
fontWeight: '800',
color: '#1e293b',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
infoBox: { marginBottom: 16 },
label: { fontSize: 12, color: '#94a3b8', fontWeight: '600', marginBottom: 4 },
value: { fontSize: 15, fontWeight: '700', color: '#334155' },
spacer: { height: 12 },
const SectionCard = ({ title, icon: Icon, children, colors, spacing, shadows }: any) => {
const styles = makeStyles(colors, spacing, shadows);
return (
<View style={styles.card}>
<View style={styles.cardHeader}>
<Icon size={18} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.text} style={styles.cardTitle as any}>
{title.toUpperCase()}
</AppText>
</View>
<View style={[styles.cardDivider, { backgroundColor: colors.border }]} />
{children}
</View>
);
};
timelineContainer: { paddingLeft: 8 },
timelineRow: { flexDirection: 'row', alignItems: 'center', gap: 15 },
iconCircle: { width: 34, height: 34, borderRadius: 10, backgroundColor: '#f1f5f9', alignItems: 'center', justifyContent: 'center' },
timelineConnector: { width: 2, height: 20, backgroundColor: '#f1f5f9', marginLeft: 16 },
const InfoRow = ({ label, value, colors, last = false, multiline = false, valueColor }: any) => (
<View style={[
{
flexDirection: 'row',
alignItems: multiline ? 'flex-start' : 'center',
justifyContent: 'space-between',
paddingVertical: 13,
borderBottomWidth: last ? 0 : StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0,0,0,0.06)',
gap: 12,
},
]}>
<AppText variant="bodySmall" color={colors.textSecondary} style={{ flex: 1 } as any}>
{label}
</AppText>
<AppText
variant="bodySmall"
color={valueColor || colors.text}
style={{ flex: 1, textAlign: 'right', fontWeight: '600' } as any}
numberOfLines={multiline ? 4 : 1}
>
{value || '—'}
</AppText>
</View>
);
tableWrapper: { backgroundColor: '#f8fafc', borderRadius: 12, padding: 12 },
tableHead: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: '#e2e8f0', paddingBottom: 8, marginBottom: 12 },
tableLabel: { fontSize: 11, fontWeight: '800', color: '#94a3b8', flex: 1 },
tableRow: { flexDirection: 'row', alignItems: 'center' },
itemMain: { fontSize: 14, fontWeight: '700', color: '#1e293b' },
itemSub: { fontSize: 12, color: '#64748b' },
itemQty: { flex: 1, fontSize: 14, fontWeight: '700', color: '#334155' },
actionIcon: { padding: 6 },
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
root: {
flex: 1,
backgroundColor: colors.background,
},
fileCard: { flexDirection: 'row', alignItems: 'center', gap: 12, backgroundColor: '#f8fafc', padding: 12, borderRadius: 12 },
fileIcon: { width: 40, height: 40, borderRadius: 8, backgroundColor: '#fee2e2', alignItems: 'center', justifyContent: 'center' },
fileName: { fontSize: 14, fontWeight: '700', color: '#334155' },
fileSize: { fontSize: 12, color: '#94a3b8' },
/* Header */
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.m,
paddingBottom: spacing.m,
backgroundColor: colors.surface,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
gap: spacing.s,
},
backBtn: {
width: 36,
height: 36,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
headerCenter: {
flex: 1,
},
headerTitle: {
fontWeight: '800',
} as any,
statusChip: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.s + 4,
paddingVertical: 6,
borderRadius: 20,
borderWidth: 1.2,
gap: 5,
},
statusDot: {
width: 7,
height: 7,
borderRadius: 4,
},
statusText: {
fontWeight: '800',
letterSpacing: 0.5,
} as any,
footer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: '#fff',
padding: 20,
paddingBottom: Platform.OS === 'ios' ? 34 : 20,
borderTopWidth: 1,
borderTopColor: '#f1f5f9',
},
mainBtn: {
backgroundColor: colors.primary,
height: 56,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 4,
},
btnDisabled: { backgroundColor: '#cbd5e1' },
mainBtnText: { color: '#fff', fontSize: 16, fontWeight: '800' },
});
scrollContent: {
padding: spacing.m,
paddingBottom: 50,
gap: spacing.m,
},
export default memo(AuctionFullDetail);
/* Card */
card: {
backgroundColor: colors.surface,
borderRadius: 18,
paddingHorizontal: spacing.m,
paddingTop: spacing.m,
paddingBottom: spacing.s,
marginBottom: spacing.m,
...shadows.light,
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
marginBottom: spacing.s + 2,
},
cardTitle: {
fontWeight: '800',
letterSpacing: 1.2,
} as any,
cardDivider: {
height: StyleSheet.hairlineWidth,
marginBottom: spacing.s,
},
/* Items */
itemRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.s + 2,
borderBottomWidth: StyleSheet.hairlineWidth,
gap: spacing.s,
},
itemLeft: {
flex: 1,
},
itemName: {
fontWeight: '700',
marginBottom: 2,
} as any,
eyeBtn: {
width: 32,
height: 32,
borderRadius: 9,
alignItems: 'center',
justifyContent: 'center',
},
/* Docs */
docRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.s + 2,
borderBottomWidth: StyleSheet.hairlineWidth,
gap: spacing.s,
},
docIcon: {
width: 32,
height: 32,
borderRadius: 9,
alignItems: 'center',
justifyContent: 'center',
},
docName: {
flex: 1,
fontWeight: '600',
} as any,
});
+67 -69
View File
@@ -1,38 +1,43 @@
import React, { memo, useState } from 'react';
import React, { memo, useState, useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform, Modal } from 'react-native';
import { Package, IndianRupee, FileText, Download, ArrowLeft, Info, } from 'lucide-react-native';
import colors from '../../../Constant/Color';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useAucitonItemDetail } from '../../../Services/ApiCall/Hooks';
import ReactNativeBlobUtil from 'react-native-blob-util';
import { AUCTION_ITEM_DOCUMENT_DOWNLOAD } from '../../../Services/ApiCall/ApiUrls';
import { Loader } from '../../../Component/Index';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props { visible: boolean; onClose: () => void; data?: any; }
const Section = ({ title, icon: Icon, children }: any) => (
const Section = ({ title, icon: Icon, children, styles, colors }: any) => (
<View style={styles.sectionCard}>
<View style={styles.sectionHeader}>
<View style={styles.iconCircle}>
<Icon size={18} color={colors.primary} strokeWidth={2.5} />
</View>
<Text style={styles.sectionTitle}>{title}</Text>
<AppText variant="caption" bold style={styles.sectionTitle}>{title}</AppText>
</View>
{children}
</View>
);
const PriceTile = ({ label, fig, words, full = false }: any) => (
const PriceTile = ({ label, fig, words, full = false, styles }: any) => (
<View style={[styles.priceTile, full ? { width: '100%' } : { width: '48%' }]}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.priceFig}>₹{Number(fig).toLocaleString('en-IN')}</Text>
<Text style={styles.priceWords}>{words}</Text>
<AppText variant="caption" color={styles.label.color} bold style={styles.label}>{label}</AppText>
<AppText variant="h3" bold style={styles.priceFig}>₹{Number(fig).toLocaleString('en-IN')}</AppText>
<AppText variant="caption" style={styles.priceWords}>{words}</AppText>
</View>
);
const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const inset = useSafeAreaInsets()
const { colors, typography } = useTheme();
const styles = useMemo(() => createStyles(colors, typography), [colors, typography]);
const [allBoolean, setAllBoolean] = useState({ fileLoading: false });
const { data: itemData } = useAucitonItemDetail({ auctionItemId: data.id });
const { data: itemData } = useAucitonItemDetail({ auctionItemId: data?.id });
const handleDownload = async (id: number, fileName: string) => {
try {
@@ -41,10 +46,7 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const path = `${dirs.DownloadDir}/${fileName}.pdf`;
const downloadUrl = `${AUCTION_ITEM_DOCUMENT_DOWNLOAD}?auctionItemId=${id}`;
const response = await ReactNativeBlobUtil.config({
fileCache: false,
@@ -83,11 +85,11 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
{/* Navbar */}
<View style={[styles.header, { paddingTop: inset.top }]}>
<TouchableOpacity onPress={onClose} style={styles.backBtn}>
<ArrowLeft size={24} color="#1e293b" />
<ArrowLeft size={24} color={colors.text} />
</TouchableOpacity>
<View>
<Text style={styles.headerTitle}>Item Information</Text>
<Text style={styles.headerSub}>Detail View</Text>
<AppText variant="body" bold style={styles.headerTitle}>Item Information</AppText>
<AppText variant="caption" style={styles.headerSub}>Detail View</AppText>
</View>
</View>
@@ -96,69 +98,66 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<Section title="Product Specification" icon={Package}>
<Section title="Product Specification" icon={Package} styles={styles} colors={colors}>
<View style={styles.identityHeader}>
<Text style={styles.productNameText}>{data.productName.toUpperCase()}</Text>
<AppText variant="h4" bold style={styles.productNameText}>{data?.productName?.toUpperCase()}</AppText>
<View style={styles.categoryBadge}>
<Text style={styles.categoryText}>{data.categoryName}</Text>
<AppText variant="caption" bold style={styles.categoryText}>{data?.categoryName}</AppText>
</View>
</View>
<View style={styles.specGrid}>
<View style={styles.specItem}>
<Text style={styles.label}>Sub Category</Text>
<Text style={styles.specValue}>{data.subCategoryName}</Text>
<AppText variant="caption" bold style={styles.label}>Sub Category</AppText>
<AppText variant="bodySmall" bold style={styles.specValue}>{data?.subCategoryName}</AppText>
</View>
<View style={styles.specItem}>
<Text style={styles.label}>Duration</Text>
<Text style={styles.specValue}>{data.timeDurationType}</Text>
<AppText variant="caption" bold style={styles.label}>Duration</AppText>
<AppText variant="bodySmall" bold style={styles.specValue}>{data?.timeDurationType}</AppText>
</View>
</View>
</Section>
<Section title="Reserve & Bid Values" icon={IndianRupee}>
<Section title="Reserve & Bid Values" icon={IndianRupee} styles={styles} colors={colors}>
<PriceTile
label="Bidding Price"
fig={data.reservePrice || data.emdAmount}
words={data.reservePriceInWords || data.emdAmountInWords}
fig={data?.reservePrice || data?.emdAmount}
words={data?.reservePriceInWords || data?.emdAmountInWords}
full
styles={styles}
/>
<View style={styles.hDivider} />
<PriceTile
label="Auction Start Value"
fig={data.reservePrice || data.auctionStartValueFigure}
words={data.reservePriceInWords || data.auctionStartValueWords}
fig={data?.reservePrice || data?.auctionStartValueFigure}
words={data?.reservePriceInWords || data?.auctionStartValueWords}
full
styles={styles}
/>
<View style={styles.hDivider} />
{/* <View style={styles.row}>
<PriceTile label="Bid Variation" fig={data.bidVariationFig} words={data.bidVariationWords} />
<PriceTile label="Auction Start Value" fig={data.auctionStartValue} words={data.auctionStartValueInWords
} />
</View> */}
</Section>
<Section title="Brief Description" icon={Info}>
<Text style={styles.descContent}>{data.briefDescription}</Text>
<Section title="Brief Description" icon={Info} styles={styles} colors={colors}>
<AppText variant="bodySmall" style={styles.descContent}>{data?.briefDescription}</AppText>
</Section>
{itemData?.length && <Section title="View Documents" icon={FileText}>
{itemData?.length && <Section title="View Documents" icon={FileText} styles={styles} colors={colors}>
<View style={styles.docTableHead}>
<Text style={styles.tableLabel}>Sr No.</Text>
<Text style={[styles.tableLabel, { flex: 2 }]}>Document Name</Text>
<Text style={styles.tableLabel}>Action</Text>
<AppText variant="caption" bold style={styles.tableLabel}>Sr No.</AppText>
<AppText variant="caption" bold style={[styles.tableLabel, { flex: 2 }]}>Document Name</AppText>
<AppText variant="caption" bold style={styles.tableLabel}>Action</AppText>
</View>
{itemData?.map((doc: any, index: any) => (
<TouchableOpacity style={styles.docRow} activeOpacity={0.7} onPress={() => { handleDownload(doc.assetId, doc.fileName) }} key={doc.assetId}>
<Text style={styles.docIndex}>{index + 1}</Text>
<AppText variant="bodySmall" bold style={styles.docIndex}>{index + 1}</AppText>
<View style={{ flex: 2 }}>
<Text style={styles.docTitle} numberOfLines={1}>{doc.fileName}</Text>
<AppText variant="bodySmall" bold style={styles.docTitle} numberOfLines={1}>{doc.fileName}</AppText>
</View>
<View style={styles.downloadIconBox}>
{allBoolean.fileLoading ? (
<View >
<Loader size='small' color='#94a3b8' />
<Loader size='small' color={colors.textSecondary} />
</View>
) : (
<Download size={20} color={colors.primary} />
@@ -173,26 +172,26 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
);
};
const styles = StyleSheet.create({
fullModal: { margin: 0, backgroundColor: '#f8fafc' },
safeArea: { flex: 1 },
const createStyles = (colors: any, typography: any) => StyleSheet.create({
fullModal: { margin: 0, backgroundColor: colors.background },
safeArea: { flex: 1, backgroundColor: colors.background },
header: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
backgroundColor: '#fff',
backgroundColor: colors.surface,
borderBottomWidth: 1,
borderBottomColor: '#f1f5f9',
borderBottomColor: colors.border,
},
backBtn: { padding: 8, marginRight: 8 },
headerTitle: { fontSize: 18, fontWeight: '800', color: '#0f172a' },
headerSub: { fontSize: 12, color: '#94a3b8', fontWeight: '600', textTransform: 'uppercase' },
headerTitle: { fontSize: 18, color: colors.text },
headerSub: { fontSize: 12, color: colors.textSecondary, textTransform: 'uppercase' },
scrollView: { flex: 1 },
scrollContent: { padding: 16, paddingBottom: 50 },
sectionCard: {
backgroundColor: '#fff',
backgroundColor: colors.surface,
borderRadius: 24,
padding: 20,
marginBottom: 16,
@@ -217,55 +216,54 @@ const styles = StyleSheet.create({
},
sectionTitle: {
fontSize: 14,
fontWeight: '800',
color: '#334155',
color: colors.text,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
// Product Identity
identityHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 },
productNameText: { fontSize: 20, fontWeight: '900', color: '#1e293b', flex: 1 },
categoryBadge: { backgroundColor: '#f1f5f9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20 },
categoryText: { fontSize: 11, fontWeight: '800', color: '#64748b', textTransform: 'uppercase' },
productNameText: { fontSize: 20, color: colors.text, flex: 1 },
categoryBadge: { backgroundColor: colors.border, paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20 },
categoryText: { fontSize: 11, color: colors.textSecondary, textTransform: 'uppercase' },
specGrid: { flexDirection: 'row', gap: 24, marginTop: 5 },
specItem: { flex: 1 },
specValue: { fontSize: 15, fontWeight: '700', color: '#334155', marginTop: 2 },
specValue: { fontSize: 15, color: colors.text, marginTop: 2 },
// Pricing
priceTile: { marginBottom: 16 },
label: { fontSize: 11, color: '#94a3b8', fontWeight: '700', textTransform: 'uppercase', marginBottom: 4 },
priceFig: { fontSize: 22, fontWeight: '900', color: colors.black },
priceWords: { fontSize: 13, color: '#64748b', fontStyle: 'italic', marginTop: 2 },
hDivider: { height: 1, backgroundColor: '#f1f5f9', marginBottom: 16 },
label: { fontSize: 11, color: colors.textSecondary, textTransform: 'uppercase', marginBottom: 4 },
priceFig: { fontSize: 22, color: colors.text },
priceWords: { fontSize: 13, color: colors.textSecondary, fontStyle: 'italic', marginTop: 2 },
hDivider: { height: 1, backgroundColor: colors.border, marginBottom: 16 },
row: { flexDirection: 'row', justifyContent: 'space-between' },
// Description
descContent: { fontSize: 15, color: '#475569', lineHeight: 22, fontWeight: '500' },
descContent: { fontSize: 15, color: colors.textSecondary, lineHeight: 22, fontFamily: typography.fonts.medium },
// Table
docTableHead: { flexDirection: 'row', paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: '#f1f5f9', marginBottom: 10 },
tableLabel: { fontSize: 11, fontWeight: '800', color: '#94a3b8', flex: 1 },
docRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#f8fafc', padding: 12, borderRadius: 12 },
docIndex: { flex: 1, fontSize: 14, fontWeight: '700', color: '#64748b' },
docTitle: { fontSize: 14, fontWeight: '700', color: '#1e293b' },
downloadIconBox: { backgroundColor: '#fff', padding: 8, borderRadius: 10, borderWidth: 1, borderColor: '#e2e8f0' },
docTableHead: { flexDirection: 'row', paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: colors.border, marginBottom: 10 },
tableLabel: { fontSize: 11, color: colors.textSecondary, flex: 1 },
docRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background, padding: 12, borderRadius: 12 },
docIndex: { flex: 1, fontSize: 14, color: colors.textSecondary },
docTitle: { fontSize: 14, color: colors.text },
downloadIconBox: { backgroundColor: colors.surface, padding: 8, borderRadius: 10, borderWidth: 1, borderColor: colors.border },
// Footer
footer: {
position: 'absolute',
bottom: 0, width: '100%',
backgroundColor: '#fff',
backgroundColor: colors.surface,
padding: 20,
paddingBottom: Platform.OS === 'ios' ? 34 : 20,
borderTopWidth: 1, borderTopColor: '#f1f5f9'
borderTopWidth: 1, borderTopColor: colors.border
},
primaryBtn: {
backgroundColor: colors.primary,
height: 56, borderRadius: 18,
flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10
},
primaryBtnText: { color: '#fff', fontSize: 16, fontWeight: '800' }
primaryBtnText: { color: colors.buttonText, fontSize: 16, fontFamily: typography.fonts.bold }
});
export default memo(AuctionItemDetail);
+1 -1
View File
@@ -64,7 +64,7 @@ const PurchaseAuction = () => {
return (
<View style={{ flex: 1 }}>
<SearchInput onChange={(val) => { setFilter(prev => ({ ...prev, auctionNumber: val })) }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} />
<SearchInput onChange={(val) => { setFilter(prev => ({ ...prev, auctionNumber: val })) }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} filter={false} />
{isPending ? <Skelton />
:
<FlatList
+442 -244
View File
@@ -1,8 +1,24 @@
import React, { use, useEffect, useState } from 'react';
import { View, Text, FlatList, StyleSheet, TouchableOpacity, Alert } from 'react-native';
import { InsideHeader, Loader } from '../../Component/Index';
import colors from '../../Constant/Color';
import { CreditCard, Calendar, User, DollarSign, ArrowRight } from 'lucide-react-native';
import React, { useState } from 'react';
import {
View,
FlatList,
StyleSheet,
TouchableOpacity,
ActivityIndicator,
} from 'react-native';
import Animated, { FadeInDown, FadeIn } from 'react-native-reanimated';
import {
Calendar,
Hash,
Wifi,
WifiOff,
Receipt,
CheckCircle2,
Gavel,
Building2,
BadgeIndianRupee,
} from 'lucide-react-native';
import { InsideHeader } from '../../Component/Index';
import { useDispatch, useSelector } from 'react-redux';
import { useOfflineAuctionPurchase, usePurchaseAuctionList } from '../../Services/ApiCall/Hooks';
import { showSuccessAlert } from '../../Constant/ShowDailog';
@@ -10,284 +26,466 @@ import { clearPurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
import Skeleton from '../../Component/Skelton/Skelton';
const dummyAuctionList = [
{
auctionId: 1,
auctionNumber: 'AUC-2025-001',
auctionPattern: 'Single Stage',
auctionDispatchDate: '25 Feb 2025',
auctionDispatcherNumber: 'DISP-001',
auctionFees: 2500,
gst: 18,
gstAmount: 450,
portalCharge: 100,
payableAmount: 3050,
},
];
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const PurchaseAuctionList = () => {
const Navigation = useNavigation()
const Dispatch = useDispatch()
const [loading, setLoading] = useState(false);
const [list, setList] = useState([]);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const LoginUser = useSelector((state: any) => state.token.token);
const { mutate, isPending } = useOfflineAuctionPurchase()
const { mutate: purchaseListMutation, isPending: purchaseListPending } = usePurchaseAuctionList();
const Navigation: any = useNavigation();
const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const [loading, setLoading] = useState(false);
const [list, setList] = useState<any[]>([]);
const calculateTotalAmount = (item: any) => {
const gstAmount =
(Number(item.auctionFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const LoginUser = useSelector((state: any) => state.token.token);
const payableAmount =
Number(item.auctionFees) + Number(item.portalCharge) + gstAmount;
return payableAmount
}
const { mutate, isPending } = useOfflineAuctionPurchase();
const { mutate: purchaseListMutation, isPending: purchaseListPending } = usePurchaseAuctionList();
const handleOfflinePurchase = async () => {
const body = {
auctionDetails: list.map((b: any) => ({
auctionDispatchDate: b?.auctionDispatchDate,
auctionDispatcherNumber: b?.auctionDispatcherNumber,
auctionFees: b?.auctionFees,
auctionId: b?.auctionId,
auctionNumber: b?.auctionNumber,
auctionPattern: b?.auctionPattern,
gst: b?.gst,
gstAmount: b?.gstAmount,
payableAmount: calculateTotalAmount(b),
portalCharge: b?.portalCharge
})),
orderNo: ''
};
const calculateTotalAmount = (item: any) => {
const gstAmount =
(Number(item.auctionFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100);
return Number(item.auctionFees) + Number(item.portalCharge) + gstAmount;
};
try {
mutate(body, {
onSuccess: (res) => {
if (res.statusCode == 200) {
showSuccessAlert(res.message)
Dispatch(clearPurchaseAuction())
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
}
},
onError: (error) => {
console.log(error, "purchse Error");
}
})
const grandTotal = list.reduce(
(sum: number, item: any) => sum + calculateTotalAmount(item),
0,
);
} catch (error) {
console.log("purchase offline error", error);
}
}
const handlePurchase = (type: 'online' | 'offline') => {
setLoading(true);
setTimeout(() => {
setLoading(false);
type === 'online' ? Alert.alert('Online Purchase Successful!') : handleOfflinePurchase();
}, 1200);
const handleOfflinePurchase = async () => {
const body = {
auctionDetails: list.map((b: any) => ({
auctionDispatchDate: b?.auctionDispatchDate,
auctionDispatcherNumber: b?.auctionDispatcherNumber,
auctionFees: b?.auctionFees,
auctionId: b?.auctionId,
auctionNumber: b?.auctionNumber,
auctionPattern: b?.auctionPattern,
gst: b?.gst,
gstAmount: b?.gstAmount,
payableAmount: calculateTotalAmount(b),
portalCharge: b?.portalCharge,
})),
orderNo: '',
};
const renderItem = ({ item }: { item: typeof dummyAuctionList[0] }) => (
<View style={styles.card}>
<View style={styles.cardHeader}>
<Text style={styles.title}>{item.auctionNumber}</Text>
<Text style={styles.pattern}>{item.auctionPattern}</Text>
</View>
<View style={styles.cardRow}>
<Calendar size={18} color={colors.primary} />
<Text style={styles.text}>Dispatch Date: {item.auctionDispatchDate}</Text>
</View>
<View style={styles.cardRow}>
<User size={18} color={colors.primary} />
<Text style={styles.text}>Dispatcher No: {item.auctionDispatcherNumber}</Text>
</View>
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>Auction Fees: ₹{item.auctionFees}</Text>
</View>
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>GST ({item.gst}%): ₹{item.gstAmount}</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: ₹{item.payableAmount}</Text>
</View>
</View>
);
const getPurchaseList = () => {
const auctionNumbers: string[] = SelectedPurchaseList.map(
(item: { auctionNumber: string }) => item.auctionNumber
);
purchaseListMutation({ auctionNumbers, contractorId: LoginUser.contractorId }, {
onSuccess: (res) => {
console.log("purchaseListMutation",res)
if (res.statusCode == 200) {
setList(res.data)
}
},
onError: (error) => {
console.log(error, "purchase list error");
}
})
try {
mutate(body, {
onSuccess: res => {
if (res.statusCode === 200) {
showSuccessAlert(res.message);
Dispatch(clearPurchaseAuction());
Navigation.dispatch(
CommonActions.reset({ index: 0, routes: [{ name: NavigationString.Home }] }),
);
}
},
onError: error => console.log(error, 'purchase Error'),
});
} catch (error) {
console.log('purchase offline error', error);
}
useFocusEffect(
React.useCallback(() => {
getPurchaseList()
}, [SelectedPurchaseList])
};
const handlePurchase = (type: 'online' | 'offline') => {
setLoading(true);
setTimeout(() => {
setLoading(false);
type === 'online' ? undefined : handleOfflinePurchase();
}, 1200);
};
const getPurchaseList = () => {
const auctionNumbers: string[] = SelectedPurchaseList.map(
(item: { auctionNumber: string }) => item.auctionNumber,
);
purchaseListMutation(
{ auctionNumbers, contractorId: LoginUser.contractorId },
{
onSuccess: res => {
if (res.statusCode === 200) setList(res.data);
},
onError: error => console.log(error, 'purchase list error'),
},
);
};
useFocusEffect(
React.useCallback(() => {
getPurchaseList();
}, [SelectedPurchaseList]),
);
const styles = makeStyles(colors, spacing, shadows);
/* ── Render Item ── */
const renderItem = ({ item, index }: { item: any; index: number }) => {
const total = calculateTotalAmount(item);
const gstAmount =
(Number(item.auctionFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100);
return (
<View style={{ flex: 1, backgroundColor: colors.white }}>
<InsideHeader title="Purchase List" showArrow />
<View style={styles.screen}>
{purchaseListPending ? <Skeleton /> : <FlatList
data={list}
renderItem={renderItem}
keyExtractor={(item) => item.auctionId.toString()}
contentContainerStyle={styles.container}
/>}
<Animated.View
entering={FadeInDown.delay(index * 90).springify()}
style={styles.card}
>
{/* Top accent stripe */}
<View style={styles.cardAccent} />
<View style={styles.cardBody}>
{/* Header */}
<View style={styles.cardHeader}>
<View style={[styles.indexBadge, { backgroundColor: colors.primary }]}>
<AppText variant="caption" color={colors.buttonText} style={{ fontWeight: '800' } as any}>
{String(index + 1).padStart(2, '0')}
</AppText>
</View>
<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')}
>
{
isPending ? <Loader size='small' color={colors.white} /> : <>
<Text style={styles.buttonText}>Purchase Offline</Text>
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} />
</>
}
</TouchableOpacity>
<View style={{ flex: 1, marginLeft: spacing.s }}>
<AppText variant="h4" color={colors.text} numberOfLines={2}>
{item.auctionNumber}
</AppText>
{item.auctionPattern ? (
<View style={[styles.patternBadge, { backgroundColor: `${colors.primary}15` }]}>
<Gavel size={11} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} style={{ fontWeight: '600' } as any}>
{item.auctionPattern}
</AppText>
</View>
) : null}
</View>
</View>
<View style={styles.divider} />
{/* Info Rows */}
<InfoRow
icon={<Hash size={15} color={colors.primary} />}
label="Dispatcher No"
value={item.auctionDispatcherNumber ?? '—'}
colors={colors}
/>
<InfoRow
icon={<Calendar size={15} color={colors.primary} />}
label="Dispatch Date"
value={item.auctionDispatchDate ?? '—'}
colors={colors}
/>
<View style={[styles.divider, { marginVertical: spacing.s }]} />
{/* Fee Breakdown */}
<View style={[styles.feeSection, { backgroundColor: colors.overlay }]}>
<AppText variant="caption" color={colors.textSecondary} style={styles.feeHeader as any}>
FEE BREAKDOWN
</AppText>
<FeeRow label="Auction Fees" value={`₹ ${Number(item.auctionFees).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, { backgroundColor: colors.primary }]}>
<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="Auction Purchase" showArrow />
{/* Grand Total Strip */}
<Animated.View entering={FadeInDown.springify()} style={styles.grandTotalStrip}>
<View>
<AppText variant="caption" color={colors.textSecondary}>
{list.length} auction{list.length !== 1 ? 's' : ''}
</AppText>
<AppText variant="h3" color={colors.text}>
₹ {grandTotal.toFixed(2)}
</AppText>
</View>
<View style={[styles.grandTotalRight, { backgroundColor: `${colors.success}15` }]}>
<CheckCircle2 size={18} color={colors.success} />
<AppText variant="caption" color={colors.success} style={{ fontWeight: '600' } as any}>
Ready to pay
</AppText>
</View>
</Animated.View>
{/* List */}
{purchaseListPending ? (
<Skeleton />
) : (
<FlatList
data={list}
renderItem={renderItem}
keyExtractor={item => item.auctionId?.toString()}
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
/>
)}
{/* Bottom Buttons */}
<View style={styles.footer}>
<TouchableOpacity
style={[styles.button, styles.onlineButton, { borderColor: colors.primary }]}
activeOpacity={0.85}
onPress={() => handlePurchase('online')}
>
<Wifi size={18} color={colors.primary} />
<AppText variant="button" color={colors.primary} style={styles.btnText as any}>
Online
</AppText>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.offlineButton, { backgroundColor: colors.buttonPrimary }]}
activeOpacity={0.85}
onPress={() => handlePurchase('offline')}
>
{isPending ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<>
<WifiOff size={18} color={colors.buttonText} />
<AppText variant="button" color={colors.buttonText} style={styles.btnText as any}>
Offline
</AppText>
</>
)}
</TouchableOpacity>
</View>
{/* Loading Overlay */}
{(loading || isPending) && (
<Animated.View entering={FadeIn.duration(200)} style={styles.loadingOverlay}>
<View style={[styles.loadingCard, { backgroundColor: colors.surface }]}>
<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 PurchaseAuctionList;
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,
},
/* Grand total strip */
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,
paddingHorizontal: spacing.s + 2,
paddingVertical: spacing.xs + 2,
borderRadius: 20,
},
container: {
padding: 16,
paddingBottom: 120,
paddingHorizontal: spacing.m,
paddingTop: spacing.s,
paddingBottom: 120,
},
/* Card */
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,
alignItems: 'center',
justifyContent: 'center',
marginTop: 2,
},
pattern: {
fontSize: 14,
color: colors.primary,
fontWeight: '600',
patternBadge: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
alignSelf: 'flex-start',
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: {
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: {
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 */
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,
},
offlineButton: {
...shadows.medium,
},
btnText: {
letterSpacing: 1.2,
} as any,
/* Loading overlay */
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: {
borderRadius: 20,
padding: spacing.l,
alignItems: 'center',
...shadows.strong,
},
});
+276 -240
View File
@@ -1,262 +1,298 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Platform } from 'react-native';
import React, { useMemo } from 'react';
import { View, StyleSheet, TouchableOpacity } 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 AuctionDetail from './Modal/AuctionDetail';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
import {
Tag,
Calendar,
IndianRupee,
ArrowRight,
CircleDot,
Clock,
TimerOff,
} from 'lucide-react-native';
const PurchaseAuctionTile = ({ item, onPressDetails, onStartBidding, buttonEnable, isAuctionEnd }: any) => {
const PurchaseAuctionTile = ({
item,
onPressDetails,
onStartBidding,
buttonEnable,
isAuctionEnd,
}: any) => {
const { colors, spacing, shadows } = useTheme();
const [showDetail, setShowDetail] = React.useState(false);
const scale = useSharedValue(1);
const elevation = useSharedValue(8);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: scale.value }],
elevation: elevation.value, // Android-safe
};
}, []);
const handlePressIn = () => {
scale.value = withTiming(0.97, { duration: 120 });
elevation.value = withTiming(3, { duration: 120 });
};
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const handlePressOut = () => {
scale.value = withTiming(1, { duration: 160 });
elevation.value = withTiming(8, { duration: 160 });
};
const handlePressIn = () => scale.value = withTiming(0.98, { duration: 140 });
const handlePressOut = () => scale.value = withTiming(1, { duration: 160 });
const statusColor = isAuctionEnd ? '#EF4444' : buttonEnable ? colors.primary : '#94A3B8';
const isAmended = item?.auctionStatus === 'Amended';
const biddingEnd = isAmended
? item?.amendedkeyDates?.auctionBidding?.endDateTime?.replace(' kk', '')
: item?.keyDates?.auctionBidding?.endDateTime?.replace(' kk', '');
const biddingStart = isAmended
? item?.amendedkeyDates?.auctionBidding?.startDateTime?.replace(' kk', '')
: item?.keyDates?.auctionBidding?.startDateTime?.replace(' kk', '');
const lightStatusBg = isAuctionEnd ? '#FEF2F2' : buttonEnable ? '#EFF6FF' : '#F8FAFC';
const statusConfig = useMemo(() => {
if (isAuctionEnd) return { label: 'ENDED', color: colors.error, bg: `${colors.error}15`, Icon: TimerOff };
if (buttonEnable) return { label: 'LIVE', color: colors.success, bg: `${colors.success}15`, Icon: CircleDot };
return { label: 'PENDING', color: colors.textSecondary, bg: `${colors.textSecondary}12`, Icon: Clock };
}, [isAuctionEnd, buttonEnable, colors]);
const styles = makeStyles(colors, spacing);
if (showDetail) {
return <AuctionDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} />;
}
return (
<View style={{ flex: 1 }}>
{!showDetail ? <Animated.View
entering={FadeInUp.duration(400)}
style={[styles.shadowContainer, animatedStyle]}
<Animated.View
entering={FadeInUp.duration(450).springify()}
style={[styles.container, shadows.medium, animatedStyle]}
>
<TouchableOpacity
activeOpacity={1}
onPress={() => setShowDetail(true)}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
style={styles.card}
>
<TouchableOpacity
activeOpacity={1}
onPress={() => setShowDetail(true)}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
style={styles.cardInternal}
>
<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 }]}>
{isAuctionEnd ? 'ENDED' : buttonEnable ? 'LIVE' : 'PENDING'}
</Text>
</View>
<Text style={styles.title}>{item.auctionNumber}</Text>
</View>
<View style={styles.amountContainer}>
<Text style={styles.amount}>
₹{item.auctionFees?.toLocaleString()}
</Text>
<Text style={styles.feeLabel}>Auction Fee</Text>
</View>
{/* ── Header ── */}
<View style={styles.header}>
<View style={styles.headerTop}>
{/* Status pill */}
<View style={[styles.statusBadge, { backgroundColor: statusConfig.bg }]}>
<statusConfig.Icon size={11} color={statusConfig.color} strokeWidth={3} />
<AppText variant="caption" color={statusConfig.color} style={styles.statusText as any}>
{statusConfig.label}
</AppText>
</View>
{/* Date */}
<View style={styles.dateRow}>
<Text style={styles.dateText}>
<Text style={styles.dateLabel}>Bidding Start: </Text>
{moment(
item?.auctionStatus === "Amended" ? item?.amendedkeyDates?.auctionBidding?.startDateTime?.replace(' kk', '') : item?.keyDates?.auctionBidding?.startDateTime?.replace(' kk', ''),
'DD-MM-YYYY HH:mm:ss'
).format('DD MMM YYYY, hh:mm A')}
</Text>
<Text style={styles.dateText}>
<Text style={styles.dateLabel}>Bidding Ends: </Text>
{moment(
item?.auctionStatus === "Amended" ? item?.amendedkeyDates?.auctionBidding?.endDateTime?.replace(' kk', '') : item?.keyDates?.auctionBidding?.endDateTime?.replace(' kk', ''),
'DD-MM-YYYY HH:mm:ss'
).format('DD MMM YYYY, hh:mm A')}
</Text>
</View>
{/* Footer */}
<View style={styles.footer}>
<View style={styles.footerTop}>
<Text style={styles.footerLabel}>Remaining Time</Text>
{buttonEnable && !isAuctionEnd ? (
<CountdownTimer
endDate={item?.auctionStatus === "Amended" ? item?.amendedkeyDates?.auctionBidding?.endDateTime : item?.keyDates?.auctionBidding?.endDateTime}
/>
) : (
<Text style={styles.endedText}>--:--:--</Text>
)}
</View>
<TouchableOpacity
disabled={!buttonEnable}
onPress={onStartBidding}
activeOpacity={0.85}
style={[
styles.actionButton,
{ backgroundColor: statusColor },
!buttonEnable && styles.disabledButton,
]}
>
<Text style={styles.addText}>
{isAuctionEnd
? 'Bidding Closed'
: !buttonEnable
? 'Pending Bid'
: 'Proceed to Start Bidding'}
</Text>
</TouchableOpacity>
{/* Auction number */}
<View style={styles.numberRow}>
<Tag size={12} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.textSecondary} style={styles.numberText as any}>
{item?.auctionNumber}
</AppText>
</View>
</View>
</TouchableOpacity>
</Animated.View> :
<AuctionDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} />
}
</View>
{/* Work title */}
<AppText variant="bodySmall" color={colors.text} numberOfLines={2} style={styles.workTitle as any}>
{item?.auctionOfWork ?? item?.auctionNumber ?? 'Auction Details'}
</AppText>
</View>
<View style={styles.divider} />
{/* ── Info Grid ── */}
<View style={styles.infoGrid}>
{/* Fee */}
<View style={styles.infoRow}>
<View style={[styles.iconBox, { backgroundColor: `${colors.primary}15` }]}>
<IndianRupee size={17} color={colors.primary} strokeWidth={2} />
</View>
<View>
<AppText variant="caption" color={colors.textSecondary}>Auction Fee</AppText>
<AppText variant="caption" color={colors.text} style={styles.infoValue as any}>
₹{Number(item?.auctionFees ?? 0).toLocaleString('en-IN')}
</AppText>
</View>
</View>
{/* Bidding End */}
<View style={styles.infoRow}>
<View style={[styles.iconBox, { backgroundColor: `${colors.primary}15` }]}>
<Calendar size={17} color={colors.primary} strokeWidth={2} />
</View>
<View>
<AppText variant="caption" color={colors.textSecondary}>Bidding Ends</AppText>
<AppText variant="caption" color={colors.text} style={styles.infoValue as any}>
{biddingEnd
? moment(biddingEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, hh:mm A')
: '—'}
</AppText>
</View>
</View>
</View>
{/* ── Footer ── */}
<View style={styles.footer}>
<AppText variant="caption" color={colors.textSecondary} style={{ marginBottom: 4, alignSelf: 'flex-start' } as any}>
Time Remaining
</AppText>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
{/* Timer */}
<View style={styles.timerSection}>
{buttonEnable && !isAuctionEnd ? (
<CountdownTimer endDate={biddingEnd} />
) : (
<AppText variant="bodySmall" color={colors.textSecondary} style={{ fontWeight: '700' } as any}>
-- : -- : --
</AppText>
)}
</View>
{/* CTA Button */}
<TouchableOpacity
disabled={!buttonEnable || isAuctionEnd}
onPress={onStartBidding}
activeOpacity={0.85}
style={[
styles.ctaBtn,
{
backgroundColor:
isAuctionEnd
? colors.error
: buttonEnable
? colors.primary
: colors.border,
opacity: (!buttonEnable || isAuctionEnd) ? 0.8 : 1,
},
]}
>
<AppText
variant="button"
color={colors.buttonText}
style={styles.ctaText as any}
>
{isAuctionEnd ? 'Ended' : !buttonEnable ? 'Pending' : 'Bid Now'}
</AppText>
{buttonEnable && !isAuctionEnd && (
<ArrowRight size={15} color={colors.buttonText} strokeWidth={2.5} />
)}
</TouchableOpacity>
</View>
</View>
</TouchableOpacity>
</Animated.View>
);
};
export default PurchaseAuctionTile;
const styles = StyleSheet.create({
shadowContainer: {
marginHorizontal: 5,
marginTop: 14,
marginBottom: 6,
borderRadius: 16,
backgroundColor: '#fff',
elevation: 8,
},
cardInternal: {
borderRadius: 16,
overflow: 'hidden',
flexDirection: 'row',
backgroundColor: '#fff',
},
statusAccent: {
width: 5,
height: '100%',
},
mainInfo: {
flex: 1,
padding: 16,
},
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 12,
},
statusBadge: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 6,
marginBottom: 6,
alignSelf: 'flex-start',
},
statusDot: {
width: 6,
height: 6,
borderRadius: 3,
marginRight: 5,
},
statusText: {
fontSize: 10,
fontWeight: '800',
letterSpacing: 0.5,
},
title: {
fontWeight: '800',
fontSize: 16,
color: '#1E293B',
},
amountContainer: {
alignItems: 'flex-end',
},
amount: {
fontWeight: '900',
fontSize: 18,
color: '#0F172A',
},
feeLabel: {
fontSize: 10,
color: '#64748B',
fontWeight: '500',
},
dateRow: {
backgroundColor: '#F8FAFC',
paddingHorizontal: 10,
paddingVertical: 8,
borderRadius: 10,
marginBottom: 16,
borderWidth: 1,
borderColor: '#F1F5F9',
},
dateText: {
fontSize: 12,
color: '#334155',
fontWeight: '600',
},
dateLabel: {
fontWeight: '400',
color: '#64748B',
},
footer: {
borderTopWidth: 1,
borderTopColor: '#F1F5F9',
paddingTop: 12,
},
footerTop: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
footerLabel: {
fontSize: 11,
fontWeight: '600',
color: '#94A3B8',
textTransform: 'uppercase',
},
endedText: {
fontSize: 13,
fontWeight: '700',
color: '#CBD5E1',
},
actionButton: {
width: '100%',
height: 48,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
}),
},
disabledButton: {
backgroundColor: '#E2E8F0',
},
addText: {
color: '#fff',
fontSize: 14,
fontWeight: '800',
letterSpacing: 0.2,
},
});
const makeStyles = (colors: any, spacing: any) =>
StyleSheet.create({
container: {
marginHorizontal: spacing.s,
marginBottom: spacing.m,
borderRadius: 16,
borderWidth: 1,
borderColor: colors.border,
backgroundColor: colors.surface,
},
card: {
borderRadius: 16,
backgroundColor: colors.surface,
padding: spacing.m,
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.03)',
},
/* Header */
header: {
marginBottom: spacing.m,
},
headerTop: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.s,
},
statusBadge: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
gap: 5,
},
statusText: {
fontWeight: '800',
fontSize: 10,
textTransform: 'uppercase',
letterSpacing: 0.5,
} as any,
numberRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
numberText: {
letterSpacing: 0.4,
fontWeight: '600',
} as any,
workTitle: {
fontWeight: '600',
lineHeight: 20,
} as any,
divider: {
height: StyleSheet.hairlineWidth,
backgroundColor: colors.border,
marginBottom: spacing.m,
opacity: 0.6,
},
/* Info grid */
infoGrid: {
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,
alignItems: 'center',
justifyContent: 'center',
},
infoValue: {
fontWeight: '700',
} as any,
/* Footer */
footer: {
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: colors.background,
marginHorizontal: -spacing.m,
marginBottom: -spacing.m,
padding: spacing.m,
borderBottomLeftRadius: 16,
borderBottomRightRadius: 16,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.border,
},
timerSection: {
flex: 1,
},
ctaBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 10,
paddingHorizontal: 18,
borderRadius: 12,
gap: 5,
},
ctaText: {
letterSpacing: 0.8,
} as any,
});
+341 -181
View File
@@ -1,227 +1,387 @@
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 {
View,
StyleSheet,
ScrollView,
TouchableOpacity,
} from 'react-native';
import Animated, {
FadeIn,
FadeOut,
FadeInDown,
Layout,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { ChevronDown, ChevronRight, Trash2, ShoppingBag, Gavel, 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 { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const PurchaseList = () => {
const Navigation: any = useNavigation()
const Dispatch = useDispatch()
const Navigation: any = useNavigation();
const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const toggleExpand = (index: number) => {
setExpandedIndex(prev => (prev === index ? null : index));
};
useEffect(() => {
if (!SelectedPurchaseList || SelectedPurchaseList.length < 1) {
Navigation.goBack();
}
}, [SelectedPurchaseList]);
useEffect(() => {
if (!SelectedPurchaseList || SelectedPurchaseList.length < 1) {
Navigation.goBack();
}
}, [SelectedPurchaseList]);
const styles = makeStyles(colors, spacing, shadows);
return (
<View style={styles.screen}>
<InsideHeader title="Purchase" showArrow />
<InsideHeader title="Purchase Auctions" showArrow />
<ScrollView contentContainerStyle={styles.container}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.heading}>Purchase List</Text>
{/* <Info size={20} color="#0F172A" /> */}
{/* 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>
{' '}auction{SelectedPurchaseList.length !== 1 ? 's' : ''} selected for purchase
</AppText>
</View>
{/* 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}
/>
<ScrollView
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
>
{SelectedPurchaseList.map((item: any, index: number) => (
<PurchaseListItem
key={item.auctionId}
item={item}
index={index}
expandedIndex={expandedIndex}
toggleExpand={toggleExpand}
colors={colors}
styles={styles}
Dispatch={Dispatch}
/>
))}
<Text style={styles.subTitle}>
Auction No: {item.auctionNumber}
</Text>
<Text style={styles.subTitle}>
Status:{' '}
<Text
style={{
color: item.status ? '#16A34A' : '#DC2626',
fontWeight: '700',
}}
>
{item.status ? 'Open' : 'Closed'}
</Text>
</Text>
</TouchableOpacity>
{/* Expanded Content */}
{expanded && (
<Animated.View
entering={FadeIn.duration(220)}
exiting={FadeOut.duration(180)}
layout={Layout.springify()}
style={styles.details}
>
{renderRow('Auction Mode', item.auctionMode)}
{renderRow('Pattern', item.auctionPattern)}
{renderRow('Fees', `₹ ${item.auctionFees}`)}
{renderRow('Department', item.deptName)}
{renderRow('Purchase End', item.purchaseEnd)}
<TouchableOpacity style={styles.removeButton} onPress={() => { Dispatch(togglePurchaseAuction(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.PurchaseAuctionList) }}>
<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.PurchaseAuctionList)}
>
<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 PurchaseList;
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 ─── */
const PurchaseListItem = ({ item, index, expandedIndex, toggleExpand, colors, styles, Dispatch }: any) => {
const expanded = expandedIndex === index;
const isActive = item.status;
const chevronStyle = useChevronStyle(expanded);
return (
<View style={styles.cardHeader}>
<Text style={styles.title}>{title}</Text>
<Animated.View style={arrowStyle}>
<ChevronDown size={22} color={colors.black} />
</Animated.View>
</View>
<Animated.View
entering={FadeInDown.delay(index * 80).springify()}
layout={Layout.springify()}
style={[styles.card, expanded && styles.cardExpanded]}
>
{/* Left Accent Bar */}
<View style={[styles.accentBar, { backgroundColor: isActive ? colors.success : colors.primary }]} />
<View style={styles.cardInner}>
<TouchableOpacity
activeOpacity={0.85}
onPress={() => toggleExpand(index)}
>
{/* Badge Row */}
<View style={styles.badgeRow}>
<View style={styles.auctionBadge}>
<Gavel size={11} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} style={styles.badgeText as any}>
{item.auctionNumber}
</AppText>
</View>
</View>
{/* Title + Chevron */}
<View style={styles.titleRow}>
<AppText variant="h4" style={styles.title as any} numberOfLines={2}>
{item.auctionOfWork ?? item.auctionNumber}
</AppText>
<Animated.View style={chevronStyle}>
<ChevronDown size={20} color={colors.textSecondary} />
</Animated.View>
</View>
{/* Dept Tag */}
<View style={styles.deptRow}>
<Tag size={12} color={colors.textSecondary} />
<AppText variant="caption" color={colors.textSecondary} style={styles.deptText as any}>
{item.deptName || 'Department'}
</AppText>
</View>
</TouchableOpacity>
{/* Expanded Section */}
{expanded && (
<Animated.View
entering={FadeIn.duration(220)}
exiting={FadeOut.duration(160)}
layout={Layout.springify()}
style={styles.detailsContainer}
>
<View style={styles.divider} />
<DetailRow label="Auction Mode" value={item.auctionMode} colors={colors} />
<DetailRow label="Pattern" value={item.auctionPattern} colors={colors} />
<DetailRow label="Auction Fees" value={`₹ ${item.auctionFees}`} colors={colors} valueColor={colors.primary} />
<DetailRow label="Department" value={item.deptName} colors={colors} />
<DetailRow label="Purchase End" value={item.purchaseEnd} colors={colors} />
{/* Remove Button */}
<TouchableOpacity
style={styles.removeButton}
activeOpacity={0.85}
onPress={() => Dispatch(togglePurchaseAuction(item))}
>
<Trash2 size={16} color="#fff" />
<AppText variant="button" color="#fff" style={styles.removeText as any}>
Remove
</AppText>
</TouchableOpacity>
</Animated.View>
)}
</View>
</Animated.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` }],
}));
}
/* ─── 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,
},
auctionBadge: {
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,
titleRow: {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
gap: spacing.s,
},
title: {
flex: 1,
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,
},
});
+50 -65
View File
@@ -1,9 +1,11 @@
import React, { memo, useCallback, useRef, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput, Alert } from 'react-native';
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import colors from '../../../Constant/Color';
import colors_const from '../../../Constant/Color';
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
import MainStyle from '../../../Styles/MainStyle';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import { Loader } from '../../../Component/Index';
import { useSelector } from 'react-redux';
import axios from 'axios';
@@ -11,6 +13,8 @@ import Variables from '../../../Constant/Variable';
import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => {
const { colors } = useTheme();
const styles = React.useMemo(() => makeStyles(colors), [colors]);
const Token = useSelector((state: any) => state.token.token)
const [showDetail, setShowDetail] = useState(false);
const [value, setValue] = useState(`${item.bidAmount}`);
@@ -100,7 +104,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
!showDetail ? <Animated.View
layout={Layout.springify()}
entering={FadeInUp.delay(100)}
style={[styles.card, styles.leadingCard, { borderColor: item.me ? '#22C55E' : 'red', backgroundColor: item.me ? "#F0FDF4" : '#fdf0f0ff' }]}
style={[styles.card, item.me ? styles.leadingCard : undefined, { borderColor: item.me ? colors.success : colors.error, backgroundColor: item.me ? `${colors.success}10` : `${colors.error}10` }]}
>
{/* HEADER */}
<TouchableOpacity
@@ -109,28 +113,28 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
style={styles.cardHeader}
>
<View style={{ flex: 1 }}>
<Text style={styles.productName}>{item.productName}</Text>
<AppText variant="h4" color={colors.text} bold style={styles.productName as any}>{item.productName}</AppText>
{/* auctionStartValueFigure */}
<Text style={styles.itemIdText}>
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
Initial Amt : {item?.auctionStartValueFigure}
</Text>
</AppText>
</View>
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
<View style={[MainStyle.flexBetween, { gap: 10 }]}>
<View style={[styles.badge, item.me ? styles.meBadge : styles.liveBadge]}>
<Text style={[styles.badgeText, item.me ? styles.meText : styles.liveText]}>
<AppText variant="caption" bold style={[styles.badgeText as any, item.me ? styles.meText : styles.liveText]}>
{item.me ? 'LEADING' : 'LIVE'}
</Text>
</AppText>
</View>
<Animated.Text style={[styles.arrow, arrowStyle]}>
<Animated.Text style={[styles.arrow, { color: colors.textSecondary }, arrowStyle]}>
▼
</Animated.Text>
</View>
<Text style={styles.itemIdText}>
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
{SelectedAuction?.auctionNumber}
</Text>
</AppText>
</TouchableOpacity>
</TouchableOpacity>
@@ -138,15 +142,15 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
{/* PRICE INFO */}
<View style={styles.priceRow}>
<View>
<Text style={styles.label}>Current Bid</Text>
<Text style={styles.mainPrice}>₹{item?.bidAmount || item.auctionStartValueFigure}</Text>
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Current Bid</AppText>
<AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item?.bidAmount || item.auctionStartValueFigure}</AppText>
</View>
<View style={{ alignItems: 'flex-end' }}>
<Text style={styles.label}>Increment</Text>
<Text style={styles.incrementText}>
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Increment</AppText>
<AppText variant="body" color={colors.primary} bold style={styles.incrementText as any}>
+₹{item.bidVariationValue}
</Text>
</AppText>
</View>
</View>
@@ -154,7 +158,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
<Animated.View style={[styles.collapsibleContainer, (!allBoolean.rebid && item.contractorId == undefined) ? bodyStyle : bodyStyle2]}>
<View style={styles.divider} />
<Text style={styles.bidLabel}>Set Your Bid</Text>
<AppText variant="bodySmall" color={colors.textSecondary} semiBold style={styles.bidLabel as any}>Set Your Bid</AppText>
{!allBoolean.oneTimeBid && <View style={styles.stepperContainer}>
<TouchableOpacity
@@ -162,29 +166,29 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
disabled={isMin}
style={[styles.stepBtn, isMin && styles.disabledBtn]}
>
<Text style={styles.stepText}>−</Text>
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>−</AppText>
</TouchableOpacity>
<View style={styles.inputBox}>
<Text style={styles.currency}>₹</Text>
<Text style={styles.bidValueText}>{bidValue}</Text>
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency as any}>₹</AppText>
<AppText variant="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
</View>
<TouchableOpacity onPress={onIncrease} style={styles.stepBtn}>
<Text style={styles.stepText}>+</Text>
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>+</AppText>
</TouchableOpacity>
</View>}
{
(allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}>
<Text style={styles.currency2}>₹</Text>
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency2 as any}>₹</AppText>
<TextInput
value={value}
onChangeText={handleChange}
keyboardType="decimal-pad"
placeholder="0.00"
style={styles.input}
placeholderTextColor="#94A3B8"
style={[styles.input, { color: colors.text }]}
placeholderTextColor={colors.textSecondary}
/>
</View>
}
@@ -194,7 +198,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
>
{
allState.isLoading ? <Loader size='small' color={colors.white} /> : <Text style={styles.submitBtnText}>Confirm Bid</Text>
allState.isLoading ? <Loader size='small' color="#FFF" /> : <AppText variant="button" color="#FFF" bold style={styles.submitBtnText as any}>Confirm Bid</AppText>
}
</TouchableOpacity> : null}
</Animated.View>
@@ -209,11 +213,11 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
export default memo(BiddingCard);
const styles = StyleSheet.create({
const makeStyles = (colors: any) => StyleSheet.create({
inputBox2: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F1F5F9',
backgroundColor: colors.background,
borderRadius: 12,
height: 45,
paddingHorizontal: 12,
@@ -221,39 +225,23 @@ const styles = StyleSheet.create({
marginBottom: 20
},
currency2: {
fontSize: 16,
fontWeight: 'bold',
color: '#64748B',
marginRight: 4,
},
} as any,
input: {
flex: 1,
fontSize: 18,
fontWeight: '700',
color: '#0F172A',
width: '100%'
},
card: {
backgroundColor: '#FFF',
borderRadius: 24,
padding: 20,
marginBottom: 16,
borderWidth: 1,
borderColor: '#E2E8F0',
overflow: 'hidden',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 10,
},
android: { elevation: 2 },
}),
},
leadingCard: {
borderColor: '#22C55E',
borderWidth: 2,
backgroundColor: '#F0FDF4',
},
cardHeader: {
flexDirection: 'row',
@@ -261,27 +249,24 @@ const styles = StyleSheet.create({
alignItems: 'flex-start',
},
headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
productName: {} as any,
itemIdText: { marginTop: 2 } as any,
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
liveBadge: { backgroundColor: '#FEE2E2' },
meBadge: { backgroundColor: '#BBF7D0' },
badgeText: { fontSize: 10, fontWeight: '900' },
liveText: { color: '#EF4444' },
meText: { color: '#15803D' },
arrow: { fontSize: 10, color: '#94A3B8' },
liveBadge: { backgroundColor: `${colors.error}20` },
meBadge: { backgroundColor: `${colors.success}20` },
badgeText: { fontSize: 10 } as any,
liveText: { color: colors.error },
meText: { color: colors.success },
arrow: { fontSize: 10 },
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
label: {
fontSize: 11,
color: '#94A3B8',
textTransform: 'uppercase',
fontWeight: '600',
},
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' },
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
} as any,
mainPrice: {} as any,
incrementText: {} as any,
collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
divider: { height: 1, backgroundColor: colors.border, marginVertical: 10 },
bidLabel: { marginBottom: 10 } as any,
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: {
width: 45,
@@ -291,19 +276,19 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
},
disabledBtn: { backgroundColor: '#CBD5E1' },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
disabledBtn: { backgroundColor: colors.border },
stepText: {} as any,
inputBox: {
flex: 1,
height: 45,
backgroundColor: '#F1F5F9',
backgroundColor: colors.background,
borderRadius: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
currency: { marginRight: 4 } as any,
bidValueText: {} as any,
submitBtn: {
height: 50,
backgroundColor: colors.primary,
@@ -311,5 +296,5 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
},
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' },
submitBtnText: {} as any,
});
@@ -0,0 +1,133 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { ShieldCheck, X } from 'lucide-react-native';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props {
visible: boolean;
onClose: () => void;
onConfirm: () => void;
bidAmount: number;
isLoading?: boolean;
}
const ConfirmBidModal = ({ visible, onClose, onConfirm, bidAmount, isLoading }: Props) => {
const { colors, shadows } = useTheme();
if (!visible) return null;
return (
<View style={styles.overlay}>
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.large }]}>
<TouchableOpacity style={[styles.closeBtn, { backgroundColor: colors.background }]} onPress={onClose} disabled={isLoading}>
<X size={18} color={colors.textSecondary} strokeWidth={2.5} />
</TouchableOpacity>
<View style={[styles.iconBox, { backgroundColor: `${colors.primary}15` }]}>
<ShieldCheck size={32} color={colors.primary} strokeWidth={2} />
</View>
<AppText variant="h3" color={colors.text} bold centered style={styles.title}>
Confirm Your Bid
</AppText>
<AppText variant="body" color={colors.textSecondary} centered style={styles.subtitle}>
Are you sure you want to place this bid? This action cannot be undone.
</AppText>
<View style={[styles.amountBox, { backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}30` }]}>
<AppText variant="caption" color={colors.primary} bold>
BID AMOUNT
</AppText>
<AppText variant="h1" color={colors.primary} bold>
₹{bidAmount?.toLocaleString('en-IN') || 0}
</AppText>
</View>
<View style={styles.btnRow}>
<TouchableOpacity style={[styles.btn, styles.cancelBtn, { backgroundColor: colors.background }]} onPress={onClose} disabled={isLoading}>
<AppText variant="button" color={colors.text} bold>Cancel</AppText>
</TouchableOpacity>
<TouchableOpacity style={[styles.btn, styles.confirmBtn, { backgroundColor: colors.primary }]} onPress={onConfirm} disabled={isLoading}>
{isLoading ? (
<ActivityIndicator color="#fff" size="small" />
) : (
<AppText variant="button" color="#fff" bold>Submit Bid</AppText>
)}
</TouchableOpacity>
</View>
</View>
</View>
);
};
export default ConfirmBidModal;
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.7)',
justifyContent: 'center',
alignItems: 'center',
padding: 24,
zIndex: 9999,
elevation: 9999,
},
modalCard: {
width: '100%',
borderRadius: 24,
padding: 24,
alignItems: 'center',
},
closeBtn: {
position: 'absolute',
top: 16,
right: 16,
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
iconBox: {
width: 64,
height: 64,
borderRadius: 32,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
title: {
marginBottom: 8,
},
subtitle: {
marginBottom: 24,
paddingHorizontal: 10,
},
amountBox: {
width: '100%',
paddingVertical: 16,
borderRadius: 16,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 24,
gap: 4,
},
btnRow: {
flexDirection: 'row',
gap: 12,
width: '100%',
},
btn: {
flex: 1,
height: 50,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
cancelBtn: {},
confirmBtn: {},
});
+189 -85
View File
@@ -1,105 +1,209 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import LottieView from 'lottie-react-native';
import Animated, { FadeInUp } from 'react-native-reanimated';
import colors from '../../../Constant/Color';
import Animated, { FadeInDown, FadeInUp, ZoomIn } from 'react-native-reanimated';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import {
ShieldCheck,
CheckCircle2,
FileText,
Gavel,
ArrowRight,
} from 'lucide-react-native';
const STEPS = [
{ icon: FileText, label: 'Documents Submitted', done: true },
{ icon: ShieldCheck, label: 'EMD Verified', done: true },
{ icon: Gavel, label: 'Ready to Bid', done: true },
];
const EmdComplete = ({ onNext }: { onNext?: () => void }) => {
return (
<View style={styles.container}>
<LottieView
source={require('../../../lottie/confetti.json')}
autoPlay
loop={true}
style={styles.confetti}
/>
const { colors, spacing, shadows } = useTheme();
<LottieView
source={require('../../../lottie/Success.json')}
autoPlay
loop={true}
style={styles.success}
/>
return (
<View style={[styles.root, { backgroundColor: colors.background }]}>
<Animated.Text entering={FadeInUp.delay(400).duration(700)} style={styles.title}>
EMD Complete
</Animated.Text>
<Animated.Text entering={FadeInUp.delay(700).duration(700)} style={styles.subText} >
Your submission was successful
</Animated.Text>
{/* ── Center content ── */}
<View style={styles.centerContent}>
<Animated.View entering={FadeInUp.delay(900).duration(700)} style={styles.nextWrapper} >
<TouchableOpacity activeOpacity={0.85} style={styles.nextBtn} onPress={onNext} >
<Text style={styles.nextText}>Next</Text>
</TouchableOpacity>
</Animated.View>
</View>
);
{/* Success animation bubble */}
<Animated.View entering={ZoomIn.delay(100).springify()} style={[styles.lottieBubble, { backgroundColor: `${colors.primary}12` }]}>
<LottieView
source={require('../../../lottie/Success.json')}
autoPlay
loop={false}
style={styles.lottie}
/>
</Animated.View>
{/* Title */}
<Animated.View entering={FadeInUp.delay(400).springify()} style={styles.titleBlock}>
<AppText variant="h2" color={colors.text} bold centered>
EMD Submitted!
</AppText>
<AppText variant="body" color={colors.textSecondary} centered style={styles.subtitle as any}>
Your Earnest Money Deposit has been{'\n'}successfully recorded.
</AppText>
</Animated.View>
{/* ── Step Completion Chips ── */}
<Animated.View entering={FadeInDown.delay(600).springify()} style={[styles.stepsCard,{backgroundColor: colors.surface, shadowColor: colors.shadow}]}>
{STEPS.map((step, index) => (
<React.Fragment key={index}>
<View style={styles.stepRow}>
<View style={[styles.stepIconBox, { backgroundColor: `${colors.success}18` }]}>
<step.icon size={16} color={colors.success} strokeWidth={2} />
</View>
<AppText variant="bodySmall" color={colors.text} semiBold style={styles.stepLabel as any}>
{step.label}
</AppText>
<CheckCircle2 size={18} color={colors.success} strokeWidth={2.5} />
</View>
{index < STEPS.length - 1 && (
<View style={[styles.stepConnector, { backgroundColor: `${colors.success}30` }]} />
)}
</React.Fragment>
))}
</Animated.View>
{/* ── Info badge ── */}
<Animated.View
entering={FadeInUp.delay(900).springify()}
style={[styles.infoBadge, { backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}30` }]}
>
<AppText variant="caption" color={colors.primary} semiBold centered>
🎉 You are now eligible to participate in the bidding process
</AppText>
</Animated.View>
</View>
{/* ── Bottom CTA ── */}
<Animated.View entering={FadeInUp.delay(1100).springify()} style={styles.bottomArea}>
<TouchableOpacity
style={[styles.ctaBtn, { backgroundColor: colors.primary, ...shadows.medium }]}
onPress={onNext}
activeOpacity={0.85}
>
<AppText variant="button" color="#fff" bold style={styles.ctaText as any}>
Continue to Bidding
</AppText>
<ArrowRight size={18} color="#fff" strokeWidth={2.5} />
</TouchableOpacity>
</Animated.View>
</View>
);
};
export default EmdComplete;
/* ---------------- Styles ---------------- */
/* ─── Styles ─── */
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F8FAFC',
alignItems: 'center',
justifyContent: 'center',
},
root: {
flex: 1,
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 40,
},
confetti: {
position: 'absolute',
top: 0,
width: '100%',
height: '100%',
},
centerContent: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 24,
gap: 24,
width: '100%',
},
success: {
width: 220,
height: 220,
marginTop: -100
},
/* Lottie bubble */
lottieBubble: {
width: 160,
height: 160,
borderRadius: 80,
alignItems: 'center',
justifyContent: 'center',
},
lottie: {
width: 130,
height: 130,
},
title: {
marginTop: 16,
fontSize: 24,
fontWeight: '900',
color: '#0F172A',
letterSpacing: 0.5,
},
/* Title */
titleBlock: {
alignItems: 'center',
gap: 8,
},
title: {
letterSpacing: 0.3,
} as any,
subtitle: {
lineHeight: 22,
opacity: 0.75,
} as any,
subText: {
marginTop: 8,
fontSize: 14,
color: '#475569',
},
/* Steps */
stepsCard: {
width: '100%',
borderRadius: 20,
paddingVertical: 16,
paddingHorizontal: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.06,
shadowRadius: 12,
elevation: 4,
},
stepRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
stepIconBox: {
width: 34,
height: 34,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
stepLabel: {
flex: 1,
} as any,
stepConnector: {
width: 2,
height: 14,
marginLeft: 16,
borderRadius: 1,
marginVertical: 2,
},
/* ---------- Floating Next Button ---------- */
nextWrapper: {
position: 'absolute',
right: 20,
bottom: 30,
},
/* Info badge */
infoBadge: {
width: '100%',
borderRadius: 16,
borderWidth: 1.2,
paddingVertical: 12,
paddingHorizontal: 16,
},
infoText: {
lineHeight: 20,
} as any,
nextBtn: {
backgroundColor: colors.primary,
paddingHorizontal: 26,
paddingVertical: 14,
borderRadius: 28,
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.35,
shadowRadius: 14,
elevation: 6,
},
nextText: {
color: '#FFFFFF',
fontSize: 15,
fontWeight: '800',
letterSpacing: 0.4,
},
/* CTA */
bottomArea: {
width: '100%',
paddingHorizontal: 24,
},
ctaBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
paddingVertical: 16,
borderRadius: 18,
},
ctaText: {
letterSpacing: 0.5,
} as any,
});
@@ -0,0 +1,108 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import { LogOut, X } from 'lucide-react-native';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props {
visible: boolean;
onClose: () => void;
onConfirm: () => void;
}
const GoBackModal = ({ visible, onClose, onConfirm }: Props) => {
const { colors, shadows } = useTheme();
if (!visible) return null;
return (
<View style={styles.overlay}>
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.large }]}>
<TouchableOpacity style={[styles.closeBtn, { backgroundColor: colors.background }]} onPress={onClose}>
<X size={18} color={colors.textSecondary} strokeWidth={2.5} />
</TouchableOpacity>
<View style={[styles.iconBox, { backgroundColor: `${colors.error}15` }]}>
<LogOut size={32} color={colors.error} strokeWidth={2} />
</View>
<AppText variant="h3" color={colors.text} bold centered style={styles.title as any}>
Leave Bidding?
</AppText>
<AppText variant="body" color={colors.textSecondary} centered style={styles.subtitle as any}>
Are you sure you want to go back to the Dashboard? You can always return to this panel later.
</AppText>
<View style={styles.btnRow}>
<TouchableOpacity style={[styles.btn, styles.cancelBtn, { backgroundColor: colors.background }]} onPress={onClose}>
<AppText variant="button" color={colors.text} bold>Stay</AppText>
</TouchableOpacity>
<TouchableOpacity style={[styles.btn, styles.confirmBtn, { backgroundColor: colors.error }]} onPress={onConfirm}>
<AppText variant="button" color="#fff" bold>Leave</AppText>
</TouchableOpacity>
</View>
</View>
</View>
);
};
export default GoBackModal;
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.7)',
justifyContent: 'center',
alignItems: 'center',
padding: 24,
zIndex: 9999,
elevation: 9999,
},
modalCard: {
width: '100%',
borderRadius: 24,
padding: 24,
alignItems: 'center',
},
closeBtn: {
position: 'absolute',
top: 16,
right: 16,
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
iconBox: {
width: 64,
height: 64,
borderRadius: 32,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
title: {
marginBottom: 8,
},
subtitle: {
marginBottom: 24,
paddingHorizontal: 10,
},
btnRow: {
flexDirection: 'row',
gap: 12,
width: '100%',
},
btn: {
flex: 1,
height: 50,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
cancelBtn: {},
confirmBtn: {},
});
@@ -1,9 +1,11 @@
import React, { memo, useCallback, useRef, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput } from 'react-native';
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import colors from '../../../Constant/Color';
import colors_const from '../../../Constant/Color';
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
import MainStyle from '../../../Styles/MainStyle';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import axios, { all } from 'axios';
import Variables from '../../../Constant/Variable';
import { useSelector } from 'react-redux';
@@ -11,6 +13,8 @@ import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
import { Loader } from '../../../Component/Index';
const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => {
const { colors } = useTheme();
const styles = React.useMemo(() => makeStyles(colors), [colors]);
const Token = useSelector((state: any) => state.token.token)
const [allState, setAllState] = useState({ isLoading: false })
const [showDetail, setShowDetail] = useState(false);
@@ -108,9 +112,10 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
entering={FadeInUp.delay(100)}
style={[
styles.card,
item.me ? styles.leadingCard : undefined,
{
borderColor: item.me ? '#22C55E' : '#EF4444',
backgroundColor: item.me ? '#F0FDF4' : '#FDF0F0',
borderColor: item.me ? colors.success : colors.error,
backgroundColor: item.me ? `${colors.success}10` : `${colors.error}10`,
},
]}
>
@@ -121,10 +126,10 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
style={styles.cardHeader}
>
<View style={{ flex: 1 }}>
<Text style={styles.productName}>{item.productName}</Text>
<Text style={styles.itemIdText}>
<AppText variant="h4" color={colors.text} bold style={styles.productName as any}>{item.productName}</AppText>
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
Initial Amt : ₹{item.auctionStartValue}
</Text>
</AppText>
</View>
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
@@ -135,38 +140,40 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
item.me ? styles.meBadge : styles.liveBadge,
]}
>
<Text
<AppText
variant="caption"
bold
style={[
styles.badgeText,
styles.badgeText as any,
item.me ? styles.meText : styles.liveText,
]}
>
{item.me ? 'LEADING' : 'LIVE'}
</Text>
</AppText>
</View>
<Animated.Text style={[styles.arrow, arrowStyle]}>
<Animated.Text style={[styles.arrow, { color: colors.textSecondary }, arrowStyle]}>
▼
</Animated.Text>
</View>
<Text style={styles.itemIdText}>
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
{SelectedAuction?.auctionNumber}
</Text>
</AppText>
</TouchableOpacity>
</TouchableOpacity>
{/* PRICE INFO */}
<View style={styles.priceRow}>
<View>
<Text style={styles.label}>Current Bid</Text>
<Text style={styles.mainPrice}>₹{item.bidAmount || item.auctionStartValue}</Text>
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Current Bid</AppText>
<AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item.bidAmount || item.auctionStartValue}</AppText>
</View>
<View style={{ alignItems: 'flex-end' }}>
<Text style={styles.label}>Decrement</Text>
<Text style={styles.incrementText}>
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Decrement</AppText>
<AppText variant="body" color={colors.primary} bold style={styles.incrementText as any}>
-₹{item.emdAmount}
</Text>
</AppText>
</View>
</View>
@@ -175,7 +182,7 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
<Animated.View style={[styles.collapsibleContainer, bodyStyle]}>
<View style={styles.divider} />
<Text style={styles.bidLabel}>Set Your Bid</Text>
<AppText variant="bodySmall" color={colors.textSecondary} semiBold style={styles.bidLabel as any}>Set Your Bid</AppText>
<View style={styles.stepperContainer}>
{/* DECREASE */}
@@ -187,25 +194,25 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
!canDecrease && styles.disabledBtn,
]}
>
<Text style={styles.stepText}>−</Text>
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>−</AppText>
</TouchableOpacity>}
{/* VALUE */}
{!allBoolean.oneTimeBid && <View style={styles.inputBox}>
<Text style={styles.currency}>₹</Text>
<Text style={styles.bidValueText}>{bidValue}</Text>
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency as any}>₹</AppText>
<AppText variant="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
</View>}
{
(allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}>
<Text style={styles.currency2}>₹</Text>
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency2 as any}>₹</AppText>
<TextInput
value={value}
onChangeText={handleChange}
keyboardType="decimal-pad"
placeholder="0.00"
style={styles.input}
placeholderTextColor="#94A3B8"
style={[styles.input, { color: colors.text }]}
placeholderTextColor={colors.textSecondary}
/>
</View>
}
@@ -219,7 +226,7 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
!canIncrease && styles.disabledBtn,
]}
>
<Text style={styles.stepText}>+</Text>
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>+</AppText>
</TouchableOpacity>}
</View>
@@ -228,7 +235,7 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
>
{
allState.isLoading ? <Loader size='small' color={colors.white} /> : <Text style={styles.submitBtnText}>Confirm Bid</Text>
allState.isLoading ? <Loader size='small' color="#FFF" /> : <AppText variant="button" color="#FFF" bold style={styles.submitBtnText as any}>Confirm Bid</AppText>
}
</TouchableOpacity>
@@ -265,50 +272,34 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
export default ReverseBiddingCard;
const styles = StyleSheet.create({
const makeStyles = (colors: any) => StyleSheet.create({
inputBox2: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F1F5F9',
backgroundColor: colors.background,
borderRadius: 12,
height: 45,
paddingHorizontal: 12,
width: '100%'
},
currency2: {
fontSize: 16,
fontWeight: 'bold',
color: '#64748B',
marginRight: 4,
},
} as any,
input: {
flex: 1,
fontSize: 18,
fontWeight: '700',
color: '#0F172A',
width: '100%'
},
card: {
backgroundColor: '#FFF',
borderRadius: 24,
padding: 20,
marginBottom: 16,
borderWidth: 1,
borderColor: '#E2E8F0',
overflow: 'hidden',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 10,
},
android: { elevation: 2 },
}),
},
leadingCard: {
borderColor: '#22C55E',
borderWidth: 2,
backgroundColor: '#F0FDF4',
},
cardHeader: {
flexDirection: 'row',
@@ -316,27 +307,24 @@ const styles = StyleSheet.create({
alignItems: 'flex-start',
},
headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
productName: {} as any,
itemIdText: { marginTop: 2 } as any,
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
liveBadge: { backgroundColor: '#FEE2E2' },
meBadge: { backgroundColor: '#BBF7D0' },
badgeText: { fontSize: 10, fontWeight: '900' },
liveText: { color: '#EF4444' },
meText: { color: '#15803D' },
arrow: { fontSize: 10, color: '#94A3B8' },
liveBadge: { backgroundColor: `${colors.error}20` },
meBadge: { backgroundColor: `${colors.success}20` },
badgeText: { fontSize: 10 } as any,
liveText: { color: colors.error },
meText: { color: colors.success },
arrow: { fontSize: 10 },
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
label: {
fontSize: 11,
color: '#94A3B8',
textTransform: 'uppercase',
fontWeight: '600',
},
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' },
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
} as any,
mainPrice: {} as any,
incrementText: {} as any,
collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
divider: { height: 1, backgroundColor: colors.border, marginVertical: 10 },
bidLabel: { marginBottom: 10 } as any,
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: {
width: 45,
@@ -346,19 +334,19 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
},
disabledBtn: { backgroundColor: '#CBD5E1' },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
disabledBtn: { backgroundColor: colors.border },
stepText: {} as any,
inputBox: {
flex: 1,
height: 45,
backgroundColor: '#F1F5F9',
backgroundColor: colors.background,
borderRadius: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
currency: { marginRight: 4 } as any,
bidValueText: {} as any,
submitBtn: {
height: 50,
backgroundColor: colors.primary,
@@ -366,5 +354,5 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
},
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' },
submitBtnText: {} as any,
});
@@ -1,5 +1,5 @@
import React, { useState } from 'react'
import { Modal, View, Text, StyleSheet, TextInput, TouchableOpacity, ScrollView } from 'react-native'
import React, { useState, useMemo } from 'react'
import { Modal, View, StyleSheet, TextInput, TouchableOpacity, ScrollView, Platform } from 'react-native'
import { Formik } from 'formik'
import * as Yup from 'yup'
import { pick } from '@react-native-documents/picker'
@@ -7,16 +7,20 @@ import { useSelector } from 'react-redux'
import { apiCall } from '../../../Axios/Axios'
import { TECHNICAL_DOCUMENT_SUBMIT, TECHNICAL_VALUE_SUBMIT } from '../../../Services/ApiCall/ApiUrls'
import { Props } from '../Type/BidType'
import colors from '../../../Constant/Color'
import { Loader } from '../../../Component/Index'
import { useNavigation } from '@react-navigation/native'
import NavigationString from '../../../Constant/NavigationString'
import { useTheme } from '../../../Hooks/useTheme'
import AppText from '../../../components/AppText'
import { Upload, FileText, X } from 'lucide-react-native'
const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }) => {
if (!template) return null
const SelectedAuction = useSelector((state: any) => state.auction.auction)
const Navigation: any = useNavigation()
const { colors, spacing, typography } = useTheme()
const styles = useMemo(() => makeStyles(colors, spacing, typography), [colors, spacing, typography])
const [loading, setLoading] = useState(false)
const initialValues = template?.labels?.reduce((acc: any, label: any) => {
acc[label.labelName] = ''
@@ -65,10 +69,10 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
if (label.labelType === '3') {
return (
<View key={label.labelName} style={styles.field}>
<Text style={styles.label}>
<AppText variant="caption" color={colors.textSecondary} style={styles.label as any}>
{label.labelName}
{label.mandatory && <Text style={styles.required}> *</Text>}
</Text>
{label.mandatory && <AppText variant="caption" color={colors.error}> *</AppText>}
</AppText>
<TouchableOpacity
style={[
@@ -79,18 +83,25 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
pickFile(label.labelName, setFieldValue)
}
>
<Text
style={[
styles.fileText,
value?.name && styles.fileSelected,
]}
>
{value?.name || 'Upload PDF'}
</Text>
{value?.name ? (
<View style={styles.fileSelectedRow}>
<FileText size={14} color={colors.success} strokeWidth={2} />
<AppText variant="caption" color={colors.text} bold style={{ flex: 1 } as any} numberOfLines={1}>
{value.name}
</AppText>
</View>
) : (
<View style={styles.fileSelectedRow}>
<Upload size={14} color={colors.textSecondary} strokeWidth={2} />
<AppText variant="caption" color={colors.textSecondary}>
Upload PDF
</AppText>
</View>
)}
</TouchableOpacity>
{showError && (
<Text style={styles.errorText}>{error}</Text>
<AppText variant="caption" color={colors.error} style={styles.errorText as any}>{error}</AppText>
)}
</View>
)
@@ -98,10 +109,10 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
return (
<View key={label.labelName} style={styles.field}>
<Text style={styles.label}>
<AppText variant="caption" color={colors.textSecondary} style={styles.label as any}>
{label.labelName}
{label.mandatory && <Text style={styles.required}> *</Text>}
</Text>
{label.mandatory && <AppText variant="caption" color={colors.error}> *</AppText>}
</AppText>
<TextInput
style={[
@@ -109,7 +120,7 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
showError && styles.errorBorder,
]}
placeholder={`Enter ${label.labelName}`}
placeholderTextColor="#9CA3AF"
placeholderTextColor={colors.textSecondary}
value={value}
onChangeText={(text) =>
setFieldValue(label.labelName, text)
@@ -122,7 +133,7 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
/>
{showError && (
<Text style={styles.errorText}>{error}</Text>
<AppText variant="caption" color={colors.error} style={styles.errorText as any}>{error}</AppText>
)}
</View>
)
@@ -210,26 +221,26 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
<View style={styles.header}>
<View style={styles.headerLeft}>
<Text style={styles.headerTitle}>Technical Documents</Text>
<Text style={styles.headerSubtitle}>
<AppText variant="h4" color={colors.text}>Technical Documents</AppText>
<AppText variant="caption" color={colors.textSecondary}>
Fill the form, Click save when you're done.
</Text>
</AppText>
</View>
<TouchableOpacity onPress={onClose} style={styles.closeBtn}>
<Text style={styles.closeText}>✕</Text>
<X size={20} color={colors.textSecondary} />
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={styles.content}>
<View style={styles.card}>
<Text style={styles.templateHeading}>Template</Text>
<AppText variant="h3" color={colors.text} centered style={{ marginBottom: 16 } as any}>Template</AppText>
<View style={styles.templateBox}>
<Text style={styles.templateName}>
<AppText variant="bodySmall" color={colors.text} bold style={{ marginBottom: 12 } as any}>
{template.templateName}
</Text>
</AppText>
<Formik
initialValues={initialValues}
@@ -264,12 +275,12 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
{loading ? (
<Loader
size="small"
color={colors.white}
color="#fff"
/>
) : (
<Text style={styles.submitText}>
<AppText variant="button" color="#fff" bold style={styles.submitText as any}>
Submit
</Text>
</AppText>
)}
</TouchableOpacity>
</View>
@@ -287,129 +298,98 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
export default TemplateFormModal
const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: '#F9FAFB',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 14,
backgroundColor: '#FFFFFF',
borderBottomWidth: 1,
borderColor: '#E5E7EB',
},
headerLeft: {
flex: 1,
},
headerTitle: {
fontSize: 16,
fontWeight: '700',
color: '#111827',
},
headerSubtitle: {
fontSize: 12,
color: '#6B7280',
marginTop: 2,
},
closeBtn: {
padding: 6,
},
closeText: {
fontSize: 18,
color: '#6B7280',
},
content: {
paddingHorizontal: 16,
},
card: {
backgroundColor: '#FFFFFF',
borderColor: '#E5E7EB',
paddingVertical: 20
},
templateHeading: {
fontSize: 18,
fontWeight: '700',
textAlign: 'center',
marginBottom: 16,
color: '#111827',
},
templateBox: {
borderWidth: 1,
borderColor: '#E5E7EB',
borderRadius: 10,
padding: 16,
},
templateName: {
fontSize: 14,
fontWeight: '700',
color: '#111827',
marginBottom: 12,
},
field: {
marginBottom: 14,
},
label: {
fontSize: 13,
fontWeight: '600',
color: '#374151',
marginBottom: 6,
},
required: {
color: '#EF4444',
},
input: {
backgroundColor: '#FFFFFF',
borderWidth: 1,
borderColor: '#E5E7EB',
borderRadius: 8,
padding: 12,
fontSize: 14,
color: '#111827',
},
filePicker: {
borderWidth: 1,
borderColor: '#E5E7EB',
borderRadius: 8,
padding: 12,
},
fileText: {
color: '#6B7280',
fontSize: 14,
},
fileSelected: {
color: '#111827',
fontWeight: '600',
},
errorText: {
color: '#EF4444',
fontSize: 12,
marginTop: 4,
},
errorBorder: {
borderColor: '#EF4444',
},
submitRow: {
marginTop: 20,
alignItems: 'flex-end',
height: 50
},
submitBtn: {
backgroundColor: colors.primary,
paddingVertical: 10,
paddingHorizontal: 22,
borderRadius: 8,
minWidth: 120,
alignItems: 'center',
},
submitText: {
color: '#FFFFFF',
fontWeight: '700',
fontSize: 14,
},
})
const makeStyles = (colors: any, spacing: any, typography: any) =>
StyleSheet.create({
screen: {
flex: 1,
backgroundColor: colors.background,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 14,
backgroundColor: colors.surface,
borderBottomWidth: 1,
borderColor: colors.border,
},
headerLeft: {
flex: 1,
},
closeBtn: {
padding: 8,
backgroundColor: colors.overlay,
borderRadius: 10,
},
content: {
paddingHorizontal: 16,
},
card: {
backgroundColor: colors.surface,
borderRadius: 18,
marginTop: spacing.m,
padding: spacing.m,
...Platform.select({
ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.06, shadowRadius: 12 },
android: { elevation: 3 },
}),
},
templateBox: {
borderWidth: 1,
borderColor: colors.border,
borderRadius: 14,
padding: 16,
},
field: {
marginBottom: 14,
},
label: {
letterSpacing: 0.5,
textTransform: 'uppercase',
marginBottom: 6,
} as any,
input: {
backgroundColor: colors.background,
borderWidth: 1.2,
borderColor: colors.border,
borderRadius: 12,
padding: 12,
fontSize: 14,
color: colors.text,
},
filePicker: {
borderWidth: 1.2,
borderColor: colors.border,
backgroundColor: colors.background,
borderRadius: 12,
padding: 12,
},
fileSelectedRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
errorText: {
marginTop: 4,
} as any,
errorBorder: {
borderColor: colors.error,
},
submitRow: {
marginTop: 20,
alignItems: 'flex-end',
height: 50,
},
submitBtn: {
backgroundColor: colors.primary,
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 14,
minWidth: 120,
alignItems: 'center',
},
submitText: {
letterSpacing: 0.5,
} as any,
})
File diff suppressed because it is too large Load Diff
+243 -252
View File
@@ -1,52 +1,74 @@
import React, { useCallback, useEffect, useState } from 'react';
import { View, FlatList, StyleSheet, Platform, Alert, BackHandler, Text } from 'react-native';
import {
View,
FlatList,
StyleSheet,
Alert,
BackHandler,
} from 'react-native';
import LottieView from 'lottie-react-native';
import Animated, { FadeInDown } from 'react-native-reanimated';
import { InsideHeader } from '../../Component/Index';
import colors from '../../Constant/Color';
import { useWebSocketService } from '../../socket/Socket';
import Variables from '../../Constant/Variable';
import { useSelector } from 'react-redux';
import { apiCall } from '../../Axios/Axios';
import { AUCTION_ONE_TIME_BID, FORWARD_AUCTION_ITEM, GET_AUCTION_DETAILS, REVERSE_AUCTION_ITEM, SELECTED_AUCTION_ITEM } from '../../Services/ApiCall/ApiUrls';
import {
AUCTION_ONE_TIME_BID,
FORWARD_AUCTION_ITEM,
GET_AUCTION_DETAILS,
REVERSE_AUCTION_ITEM,
SELECTED_AUCTION_ITEM,
} from '../../Services/ApiCall/ApiUrls';
import BiddingCard from './Component/BiddingCard';
import ConfirmBidModal from './Component/ConfirmBidModal';
import GoBackModal from './Component/GoBackModal';
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
import CountdownTimer from '../../Component/Counter/CountdownTimer';
import FontsFamily from '../../Constant/FontsFamily';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import { Gavel, Building2, Clock4 } from 'lucide-react-native';
const StartBidding = () => {
const { colors, spacing, shadows } = useTheme();
const SelectedAuction = useSelector((state: any) => state.auction.auction);
const loginUser = useSelector((state: any) => state.token.token)
const Navigation: any = useNavigation()
const loginUser = useSelector((state: any) => state.token.token);
const Navigation: any = useNavigation();
const [items, setItems] = useState<any>([]);
const [bidValues, setBidValues] = useState<Record<number, number>>({});
const [allBoolean, setAllBoolean] = useState({ showSuccess: false, showDialog: false, bidPayload: {}, oneTimeBid: false, rebid: false, reload: false })
const [allBoolean, setAllBoolean] = useState({
showSuccess: false,
showDialog: false,
showBackDialog: false,
bidPayload: {} as any,
oneTimeBid: false,
rebid: false,
reload: false,
});
const { subscribe, createRoom, placeBid } = useWebSocketService(
Variables.webSocketUrl,
() => console.log('Connected!'),
(error) => console.log('WebSocket Error:', error),
(error: any) => console.log('WebSocket Error:', error),
loginUser.token,
);
const getAuction = async () => {
const selectedRes = await apiCall<any>('get', GET_AUCTION_DETAILS, {}, { id: SelectedAuction.auctionId });
if (selectedRes.statusCode == 200 && selectedRes.data.contractorId.length) {
setAllBoolean((prev: any) => ({ ...prev, oneTimeBid: true }))
if (selectedRes.statusCode === 200 && selectedRes.data.contractorId.length) {
setAllBoolean(prev => ({ ...prev, oneTimeBid: true }));
}
}
};
const getOneTimeBidValue = async () => {
try {
const selectedRes = await apiCall<any>('get', AUCTION_ONE_TIME_BID, {}, { auctionId: SelectedAuction.auctionId });
if (selectedRes.statusCode == 200) {
return selectedRes.data
} else {
return []
}
} catch (error) {
return selectedRes.statusCode === 200 ? selectedRes.data : [];
} catch { return []; }
};
}
}
const getAllAcutionItems = async () => {
try {
const payload = { auctionNumber: SelectedAuction.auctionNumber };
@@ -58,87 +80,40 @@ const StartBidding = () => {
}
const selectedRes = await apiCall<any>('get', SELECTED_AUCTION_ITEM, {}, payload);
const uniqueArray: number[] = Array.from(new Set(selectedRes?.data || []));
const filteredItems = auctionItem.filter(item => uniqueArray.includes(item.id));
const uniqueArray: number[] = Array.from(
new Set(selectedRes?.data || [])
);
const filteredItems = auctionItem.filter(item =>
uniqueArray.includes(item.id)
);
if (SelectedAuction.auctionCategories == 'Live') {
if (SelectedAuction.auctionCategories === 'Live') {
const result = await createRoom(SelectedAuction.auctionNumber);
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, data => {
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, (data: any) => {
console.log('🔥 BID UPDATE:', data);
});
const bidAmountMap2 = new Map<number, { bidAmount: number; contractorId: number }>();
(result || []).forEach((r: any) => {
bidAmountMap2.set(r.auctionItemId, {
bidAmount: r.bidAmount,
contractorId: r.contractorId,
});
bidAmountMap2.set(r.auctionItemId, { bidAmount: r.bidAmount, contractorId: r.contractorId });
});
// Check One Time bid enable butoon
if (SelectedAuction.auctionCategories != 'Live') {
const myBid = result.find(
(bid: any) => bid.contractorId === loginUser.contractorId
);
if (myBid) {
setAllBoolean((prev: any) => ({ ...prev, oneTimeBid: true }))
}
}
const bidAmountMap = new Map<number, number>();
(result || []).forEach((r: any) => {
bidAmountMap.set(r.auctionItemId, r.bidAmount);
});
const updatedFilteredItems = filteredItems.map(item => {
const bidData = bidAmountMap2.get(item.id);
return {
...item,
bidAmount: bidData?.bidAmount ?? item.auctionStartValueFigure,
bidAvilable: bidData?.bidAmount ?? 0,
me: bidData?.contractorId == loginUser.contractorId ? true : false
me: bidData?.contractorId === loginUser.contractorId,
};
});
const initialBids: Record<number, number> = {};
updatedFilteredItems.forEach(item => {
initialBids[item.id] = item.bidAmount;
});
updatedFilteredItems.forEach(item => { initialBids[item.id] = item.bidAmount; });
setBidValues(initialBids);
setItems(updatedFilteredItems);
} else {
let result = []
if (SelectedAuction.rebidAllowed) {
setAllBoolean((prev: any) => ({ ...prev, rebid: true }))
}
if (SelectedAuction.auctionCategories != 'Live') {
result = await getOneTimeBidValue()
}
if (SelectedAuction.rebidAllowed) setAllBoolean(prev => ({ ...prev, rebid: true }));
let result: any[] = [];
if (SelectedAuction.auctionCategories !== 'Live') result = await getOneTimeBidValue();
const bidMap = new Map<number, any>();
result.forEach((bid: any) => {
bidMap.set(bid.auctionItemId, bid);
});
result.forEach((bid: any) => bidMap.set(bid.auctionItemId, bid));
const mergedData = auctionItem.map((item: any) => {
const bid = bidMap.get(item.id);
if (!bid) {
return {
...item,
bidAmount: item.auctionStartValueFigure,
contractorId: item.contractorId,
};
}
if (!bid) return { ...item, bidAmount: item.auctionStartValueFigure, contractorId: item.contractorId };
return {
...item,
bidAmount: bid.bidAmount,
@@ -150,56 +125,36 @@ const StartBidding = () => {
};
});
const initialBids: Record<number, number> = {};
mergedData.forEach(item => {
initialBids[item.id] = item.bidAmount;
});
setBidValues(initialBids);
setItems(mergedData)
mergedData.forEach(item => { initialBids[item.id] = item.bidAmount; });
setBidValues(initialBids);
setItems(mergedData);
}
} catch (error) {
console.error('Error fetching auction items:', error);
}
};
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, async (message) => {
const result = message[message.length - 1]
const updatedBidItem = (prev: any[]) =>
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, async (message: any) => {
const result = message[message.length - 1];
setItems((prev: any[]) =>
prev.map(item =>
item.id === result.auctionItemId
? { ...item, bidAmount: result.bidAmount, me: result.contractorId == loginUser.contractorId ? true : false }
: item
);
setItems(updatedBidItem);
setBidValues(prev => ({
...prev,
[result.auctionItemId]: result.bidAmount,
}));
try {
} catch (err) {
console.log('JSON parse error:', err);
}
? { ...item, bidAmount: result.bidAmount, me: result.contractorId === loginUser.contractorId }
: item,
),
);
setBidValues(prev => ({ ...prev, [result.auctionItemId]: result.bidAmount }));
});
useEffect(() => {
if (SelectedAuction.auctionCategories == 'One-time') {
getAuction()
}
if (SelectedAuction.auctionCategories === 'One-time') getAuction();
getAllAcutionItems();
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, (data) => { });
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, () => {});
}, [allBoolean.reload]);
useEffect(() => {
const initial: Record<number, number> = {};
items.forEach((item: any) => {
initial[item.id] = item.bidAmount + item.bidVariationValue;
});
items.forEach((item: any) => { initial[item.id] = item.bidAmount + item.bidVariationValue; });
setBidValues(initial);
}, []);
@@ -216,51 +171,14 @@ const StartBidding = () => {
fullName: loginUser.fullName,
roomId: SelectedAuction.auctionNumber,
};
setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, }));
Alert.alert(
'Confirm Bid',
`Are you sure you want to place a bid of ₹${payload.bidAmount}?`,
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Submit',
onPress: () => {
placeBidSubmit(payload);
},
},
],
{ cancelable: true }
);
setAllBoolean(prev => ({ ...prev, bidPayload: payload, showDialog: true }));
};
const placeBidSubmit = async (data: any) => {
setAllBoolean(prev => ({ ...prev, showDialog: false }));
try {
const result: any = await placeBid(data);
if (result?.status && result.data) {
const updatedBidItem = (prev: any[]) =>
prev.map(item =>
item.id === result.data.auctionItemId
? { ...item, bidAmount: result.data.bidAmount, me: result.data?.contractorId == loginUser.contractorId ? true : false }
: item
);
// setItems(updatedBidItem);
// setBidValues((prev: any) => ({
// ...prev,
// [result.data.auctionItemId]: {
// ...prev[result.data.auctionItemId],
// bidAmount: result.data.bidAmount,
// },
// }));
getAllAcutionItems()
}
if (result?.status && result.data) getAllAcutionItems();
} catch (error) {
console.error('Error placing bid:', error);
}
@@ -269,55 +187,75 @@ const StartBidding = () => {
useFocusEffect(
useCallback(() => {
const onBackPress = () => {
Alert.alert(
'Confirm',
'Do you want to go back to Dashboard?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Yes',
onPress: () => {
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
},
},
],
{ cancelable: true }
);
return true; // block default back action
setAllBoolean(prev => ({ ...prev, showBackDialog: true }));
return true;
};
const subscription = BackHandler.addEventListener(
'hardwareBackPress',
onBackPress
);
return () => subscription.remove();
}, [Navigation])
const sub = BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () => sub.remove();
}, [Navigation]),
);
return (
<View style={styles.container}>
<InsideHeader title="Bidding Panel" showArrow />
<View style={styles.container2}>
<Text style={styles.title}>
{SelectedAuction.deptName}
</Text>
const styles = makeStyles(colors, spacing, shadows);
<View style={styles.timerWrapper}>
const biddingEndDate =
SelectedAuction?.auctionStatus === 'Amended'
? SelectedAuction?.amendedkeyDates?.auctionBidding?.endDateTime
: SelectedAuction?.keyDates?.auctionBidding?.endDateTime;
return (
<View style={styles.root}>
<InsideHeader title="Bidding Panel" showArrow />
{/* ── Top Info Band ── */}
<Animated.View entering={FadeInDown.springify()} style={styles.infoBand}>
{/* Department row */}
<View style={styles.deptRow}>
<View style={[styles.deptIcon, { backgroundColor: `${colors.primary}15` }]}>
<Building2 size={16} color={colors.primary} strokeWidth={2} />
</View>
<AppText variant="bodySmall" color={colors.text} bold numberOfLines={1} style={styles.deptName as any}>
{SelectedAuction.deptName ?? 'Auction Department'}
</AppText>
</View>
{/* Timer card */}
<View style={[styles.timerCard, { backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}25` }]}>
<View style={styles.timerLabelRow}>
<Clock4 size={13} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} bold style={styles.timerLabel as any}>
BIDDING ENDS IN
</AppText>
</View>
<CountdownTimer
endDate={SelectedAuction.keyDates.auctionBidding.endDateTime}
myStyle={{ fontSize: 30 }}
size={30}
endDate={biddingEndDate}
myStyle={{ fontSize: 28, fontWeight: '900', color: colors.primary, letterSpacing: 1 }}
size={28}
/>
</View>
</View>
{/* Auction chip */}
<View style={styles.chipRow}>
<View style={[styles.chip, { backgroundColor: `${colors.primary}12` }]}>
<Gavel size={12} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} bold style={styles.chipText as any}>
{SelectedAuction.auctionNumber}
</AppText>
</View>
<View style={[styles.chip, { backgroundColor: SelectedAuction.auctionCategories === 'Live' ? `${colors.success}15` : `${colors.textSecondary}12` }]}>
<View style={[styles.liveDot, { backgroundColor: SelectedAuction.auctionCategories === 'Live' ? colors.success : colors.textSecondary }]} />
<AppText
variant="caption"
color={SelectedAuction.auctionCategories === 'Live' ? colors.success : colors.textSecondary}
bold
style={styles.chipText as any}
>
{SelectedAuction.auctionCategories ?? 'Auction'}
</AppText>
</View>
</View>
</Animated.View>
{/* ── Items List ── */}
<FlatList
data={items}
keyExtractor={item => item.id.toString()}
@@ -327,15 +265,17 @@ const StartBidding = () => {
bidValue={bidValues[item.id] || 0}
onIncrease={() => handleUpdate(item.id, item.bidVariationValue)}
onDecrease={() => handleUpdate(item.id, -item.bidVariationValue)}
onPlaceBid={() => { submitBid(item) }}
onPlaceBid={() => submitBid(item)}
SelectedAuction={SelectedAuction}
allBoolean={allBoolean}
setAllBoolean={setAllBoolean}
/>
)}
contentContainerStyle={{ padding: 16, paddingBottom: 300 }}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
/>
{/* ── Success overlay ── */}
{allBoolean.showSuccess && (
<View style={styles.overlay}>
<LottieView
@@ -346,66 +286,117 @@ const StartBidding = () => {
/>
</View>
)}
<ConfirmBidModal
visible={allBoolean.showDialog}
onClose={() => setAllBoolean(prev => ({ ...prev, showDialog: false }))}
onConfirm={() => placeBidSubmit(allBoolean.bidPayload)}
bidAmount={allBoolean.bidPayload?.bidAmount}
/>
<GoBackModal
visible={allBoolean.showBackDialog}
onClose={() => setAllBoolean(prev => ({ ...prev, showBackDialog: false }))}
onConfirm={() => {
setAllBoolean(prev => ({ ...prev, showBackDialog: false }));
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
}}
/>
</View>
);
};
export default StartBidding;
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.white },
container2: {
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 8,
},
title: {
fontSize: 18,
fontFamily: FontsFamily.poppinsSemiBold,
color: colors.black,
marginBottom: 6,
// textAlign:'center'
},
timerWrapper: {
alignItems: 'center',
},
card: {
backgroundColor: '#FFF',
borderRadius: 24,
padding: 20,
marginBottom: 16,
borderWidth: 1,
borderColor: '#E2E8F0',
overflow: 'hidden',
...Platform.select({ ios: { shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 }, android: { elevation: 2 } }),
},
leadingCard: { borderColor: '#22C55E', borderWidth: 2, backgroundColor: '#F0FDF4' },
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' },
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
liveBadge: { backgroundColor: '#FEE2E2' },
meBadge: { backgroundColor: '#BBF7D0' },
badgeText: { fontSize: 10, fontWeight: '900' },
liveText: { color: '#EF4444' },
meText: { color: '#15803D' },
arrow: { fontSize: 10, color: '#94A3B8' },
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
label: { fontSize: 11, color: '#94A3B8', textTransform: 'uppercase', fontWeight: '600' },
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' },
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: { width: 45, height: 45, backgroundColor: colors.primary, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
disabledBtn: { backgroundColor: '#CBD5E1' },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
inputBox: { flex: 1, height: 45, backgroundColor: '#F1F5F9', borderRadius: 12, flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
submitBtn: { height: 50, backgroundColor: colors.primary, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' },
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(255,255,255,0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 99 },
});
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
root: {
flex: 1,
backgroundColor: colors.background,
},
/* Info band */
infoBand: {
backgroundColor: colors.surface,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
gap: spacing.s,
...shadows.light,
},
deptRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
},
deptIcon: {
width: 30,
height: 30,
borderRadius: 9,
alignItems: 'center',
justifyContent: 'center',
},
deptName: {
flex: 1,
} as any,
/* Timer */
timerCard: {
borderRadius: 16,
borderWidth: 1.2,
paddingVertical: spacing.m,
paddingHorizontal: spacing.m,
alignItems: 'center',
gap: 4,
},
timerLabelRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
timerLabel: {
letterSpacing: 1.2,
} as any,
/* Chips */
chipRow: {
flexDirection: 'row',
gap: spacing.s,
},
chip: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingHorizontal: spacing.s + 4,
paddingVertical: 5,
borderRadius: 20,
},
chipText: {
letterSpacing: 0.3,
} as any,
liveDot: {
width: 6,
height: 6,
borderRadius: 3,
},
listContent: {
padding: spacing.m,
paddingBottom: 120,
},
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: colors.overlay,
justifyContent: 'center',
alignItems: 'center',
zIndex: 99,
},
});
+37 -88
View File
@@ -11,16 +11,21 @@ import { AUCTION_ONE_TIME_BID, GET_AUCTION_DETAILS, REVERSE_AUCTION_ITEM, SELECT
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
import CountdownTimer from '../../Component/Counter/CountdownTimer';
import FontsFamily from '../../Constant/FontsFamily';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import ReverseBiddingCard from './Component/ReverseBiddingCard';
import ConfirmBidModal from './Component/ConfirmBidModal';
import GoBackModal from './Component/GoBackModal';
const StartReverseBidding = () => {
const { colors } = useTheme();
const styles = React.useMemo(() => makeStyles(colors), [colors]);
const SelectedAuction = useSelector((state: any) => state.auction.auction);
const loginUser = useSelector((state: any) => state.token.token)
const Navigation: any = useNavigation()
const [items, setItems] = useState<any>([]);
const [bidValues, setBidValues] = useState<Record<number, number>>({});
const [allBoolean, setAllBoolean] = useState({ showSuccess: false, showDialog: false, bidPayload: {}, oneTimeBid: false, rebid: false, reload: false })
const [allBoolean, setAllBoolean] = useState({ showSuccess: false, showDialog: false, showBackDialog: false, bidPayload: {} as any, oneTimeBid: false, rebid: false, reload: false })
const { subscribe, createRoom, placeBid } = useWebSocketService(
Variables.webSocketUrl,
() => console.log('Connected!'),
@@ -216,27 +221,11 @@ const StartReverseBidding = () => {
fullName: loginUser.fullName,
roomId: SelectedAuction.auctionNumber,
};
setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, }));
Alert.alert(
'Confirm Bid',
`Are you sure you want to place a bid of ₹${payload.bidAmount}?`,
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Submit',
onPress: () => {
placeBidSubmit(payload);
},
},
],
{ cancelable: true }
);
setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, showDialog: true }));
};
const placeBidSubmit = async (data: any) => {
setAllBoolean((prev: any) => ({ ...prev, showDialog: false }));
try {
const result: any = await placeBid(data);
@@ -258,26 +247,7 @@ const StartReverseBidding = () => {
useFocusEffect(
useCallback(() => {
const onBackPress = () => {
Alert.alert(
'Confirm',
'Do you want to go back to Dashboard?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Yes',
onPress: () => {
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
},
},
],
{ cancelable: true }
);
setAllBoolean((prev: any) => ({ ...prev, showBackDialog: true }));
return true; // block default back action
};
@@ -294,9 +264,9 @@ const StartReverseBidding = () => {
<View style={styles.container}>
<InsideHeader title="Bidding Panel" showArrow />
<View style={styles.container2}>
<Text style={styles.title}>
<AppText variant="h3" color={colors.text} bold style={styles.title as any}>
{SelectedAuction.deptName}
</Text>
</AppText>
<View style={styles.timerWrapper}>
<CountdownTimer
@@ -335,66 +305,45 @@ const StartReverseBidding = () => {
/>
</View>
)}
<ConfirmBidModal
visible={allBoolean.showDialog}
onClose={() => setAllBoolean(prev => ({ ...prev, showDialog: false }))}
onConfirm={() => placeBidSubmit(allBoolean.bidPayload)}
bidAmount={allBoolean.bidPayload?.bidAmount}
/>
<GoBackModal
visible={allBoolean.showBackDialog}
onClose={() => setAllBoolean(prev => ({ ...prev, showBackDialog: false }))}
onConfirm={() => {
setAllBoolean(prev => ({ ...prev, showBackDialog: false }));
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
}}
/>
</View>
);
};
export default StartReverseBidding;
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.white },
const makeStyles = (colors: any) => StyleSheet.create({
container: { flex: 1, backgroundColor: colors.background },
container2: {
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 8,
},
title: {
fontSize: 18,
fontFamily: FontsFamily.poppinsSemiBold,
color: colors.black,
marginBottom: 6,
// textAlign:'center'
},
} as any,
timerWrapper: {
alignItems: 'center',
},
card: {
backgroundColor: '#FFF',
borderRadius: 24,
padding: 20,
marginBottom: 16,
borderWidth: 1,
borderColor: '#E2E8F0',
overflow: 'hidden',
...Platform.select({ ios: { shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 }, android: { elevation: 2 } }),
},
leadingCard: { borderColor: '#22C55E', borderWidth: 2, backgroundColor: '#F0FDF4' },
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' },
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
liveBadge: { backgroundColor: '#FEE2E2' },
meBadge: { backgroundColor: '#BBF7D0' },
badgeText: { fontSize: 10, fontWeight: '900' },
liveText: { color: '#EF4444' },
meText: { color: '#15803D' },
arrow: { fontSize: 10, color: '#94A3B8' },
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
label: { fontSize: 11, color: '#94A3B8', textTransform: 'uppercase', fontWeight: '600' },
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' },
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: { width: 45, height: 45, backgroundColor: colors.primary, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
disabledBtn: { backgroundColor: '#CBD5E1' },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
inputBox: { flex: 1, height: 45, backgroundColor: '#F1F5F9', borderRadius: 12, flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
submitBtn: { height: 50, backgroundColor: colors.primary, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' },
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(255,255,255,0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 99 },
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: colors.overlay, justifyContent: 'center', alignItems: 'center', zIndex: 99 },
});
+144 -261
View File
@@ -1,308 +1,191 @@
import React, { useCallback, useEffect, useState } from 'react';
import { View, Text, StyleSheet, Dimensions, FlatList, ActivityIndicator, Alert } from 'react-native';
import Animated, { useSharedValue, useAnimatedStyle, withDelay, withTiming } from 'react-native-reanimated';
import { Layers, Clock, ShoppingCart, X, FileText } from 'lucide-react-native';
import { Header } from '../../Component/Index';
import colors from '../../Constant/Color';
import { View, StyleSheet, Dimensions, FlatList, ActivityIndicator, Alert, ScrollView, ImageBackground, TouchableOpacity, Platform } from 'react-native';
import Animated, { FadeInDown, FadeInUp } from 'react-native-reanimated';
import { Layers, Clock, ShoppingCart, X, FileText, ArrowUpRight, Menu, Radio } from 'lucide-react-native';
import { useTheme } from '../../Hooks/useTheme';
import { useDashboard, useESetuAdharVarification, useTenderDashboard } from '../../Services/ApiCall/Hooks';
import { useSelector } from 'react-redux';
import ConfirmDialog from '../../Constant/Dialog/ConfirmDialog';
import { generateTransactionId } from '../../Helper/uniqueId';
import { getToken } from '../../Redux/Slices/Token';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
import ImagePath from '../../Constant/ImagePath';
import AppText from '../../components/AppText';
import SidebarModal from '../../components/SidebarModal';
// Header is now integrated or minimal
// import { Header } from '../../Component/Index';
const { width } = Dimensions.get('window');
const CARD_WIDTH = (width - 48) / 2;
const StatCard = ({ item, index, isLoading }: any) => {
const translateY = useSharedValue(30);
const opacity = useSharedValue(0);
useEffect(() => {
translateY.value = withDelay(index * 80, withTiming(0, { duration: 400 }));
opacity.value = withDelay(index * 80, withTiming(1, { duration: 400 }));
}, []);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
opacity: opacity.value,
}));
const Icon = item.icon;
return (
<Animated.View style={[styles.card, animatedStyle]}>
<View style={styles.header}>
<Text style={styles.title} numberOfLines={2}>
{item.title}
</Text>
<View style={[styles.badge, { borderColor: item.color }]}>
<Icon size={14} color={item.color} />
<Text style={[styles.badgeText, { color: item.color }]}>
{item.value}
</Text>
</View>
</View>
{isLoading ? (
<ActivityIndicator
size="small"
color={item.color}
style={{ marginTop: 12 }}
/>
) : (
<Text style={styles.value}>{item.value}</Text>
)}
<Text style={styles.desc}>{item.desc}</Text>
</Animated.View>
);
};
const Home = () => {
const { data, isLoading } = useDashboard();
const { data: tenderData, isLoading: tenderLoading } = useTenderDashboard();
const { colors, spacing, typography, shadows } = useTheme();
const [stats, setStats] = useState<any>({});
const [isMenuVisible, setIsMenuVisible] = useState(false);
const navigation = useNavigation<any>();
const userData = useSelector(getToken);
const userType = userData?.userType; // 'tender' | 'auction'
// Hooks for data
const { data: dashboardData, refetch: refetchDashboard, isPending: isLoadingDashboard } = useDashboard();
const { data: tenderData, refetch: refetchTender, isPending: isLoadingTender } = useTenderDashboard();
const { mutate: verifyAadhaar } = useESetuAdharVarification();
useFocusEffect(
useCallback(() => {
if (userType === 'auction') refetchDashboard();
else refetchTender();
}, [userType])
);
const Navigation: any = useNavigation();
const user = useSelector((state: any) => state.token?.token);
const { mutateAsync: eSetuAdharVarification, isPending } = useESetuAdharVarification();
const dashboard = data?.data || {};
const [showDialog, setShowDialog] = useState<boolean>(false);
const [isNavigating, setIsNavigating] = useState<boolean>(false);
const [DASHBOARD_CARDS,setDASHBOARD_CARDS]=useState<any>([])
useFocusEffect(
useCallback(() => {
setIsNavigating(false);
return () => {
// cleanup if needed
};
}, [])
);
useEffect(() => {
if (user?.esetuVerified === 'Not Verified') {
setShowDialog(true);
if (userType === 'auction' && dashboardData) {
console.log(dashboardData.data, "lkkllklklklklklklklklk")
setStats(dashboardData.data || {});
} else if (userType === 'tender' && tenderData) {
console.log(tenderData.data, "bonds")
setStats(tenderData.data || {});
}
}, []);
const handleProceedVerification = async () => {
try {
setIsNavigating(true);
const payload = {
contId: user.id,
transactionId: generateTransactionId(),
udf1: "23.77",
udf2: "77.34",
udf3: "string",
udf4: "string",
};
}, [dashboardData, tenderData, userType]);
const res = await eSetuAdharVarification(payload);
const renderGlassCard = (title: string, value: string | number, icon: React.ReactNode, index: number) => (
<Animated.View
entering={FadeInUp.duration(600).delay(index * 100)}
style={[styles.glassCard, {
backgroundColor: colors.glass,
borderColor: colors.overlay,
}]}
>
<View style={styles.cardHeader}>
<View style={[styles.iconContainer, { backgroundColor: colors.overlay }]}>
{icon}
</View>
<ArrowUpRight size={20} color={colors.textSecondary} />
</View>
<AppText variant="h2" color={colors.primary}>{value || '0'}</AppText>
<AppText variant="caption" color={colors.textSecondary}>{title}</AppText>
</Animated.View>
);
setShowDialog(false);
Navigation.navigate(NavigationString.KycWebViewScreen, { res });
} catch (err) {
Alert.alert('Error', 'Failed to initiate verification. Please try again.');
setIsNavigating(false);
}
};
useEffect(()=>{
if (user?.userType === "auction") {
setDASHBOARD_CARDS([
{
title: 'Total Auctions',
value: dashboard?.myAuctionCount ?? 0,
desc: 'Auctions purchased by you',
icon: Layers,
color: '#2563EB',
},
{
title: 'Live Auctions',
value: dashboard?.liveAuctionCount ?? 0,
desc: 'Currently live',
icon: Clock,
color: '#0284C7',
},
{
title: 'Participated Auctions',
value: dashboard?.currentBiddingCount ?? 0,
desc: 'Participated in auction',
icon: ShoppingCart,
color: '#7C3AED',
},
{
title: 'Cancelled Auctions',
value: dashboard?.cancelledAuctionCount ?? 0,
desc: 'Cancelled by department',
icon: X,
color: '#DC2626',
},
]);
} else {
setDASHBOARD_CARDS([
{
title: 'Total Tenders',
value: tenderData?.data?.[0]?.totalTender ?? 0,
desc: 'Tenders assigned',
icon: FileText,
color: '#16A34A',
},
{
title: 'Live Tenders',
value: tenderData?.data?.[0]?.liveTender ?? 0,
desc: 'Currently live',
icon: Clock,
color: '#059669',
},
{
title: 'Participated Tenders',
value: tenderData?.data?.[0]?.totalParticipate ?? 0,
desc: 'Participated in tender',
icon: ShoppingCart,
color: '#0EA5E9',
},
{
title: 'Cancelled Tenders',
value: tenderData?.data?.[0]?.cancelledTender ?? 0,
desc: 'Cancelled by authority',
icon: X,
color: '#EF4444',
},
]);
}
},[tenderData,dashboard])
const StatGrid = () => (
<View style={styles.gridContainer}>
{userType === 'auction' ? (
<>
{renderGlassCard('Live Auctions', stats?.liveAuctionCount, <Radio size={24} color={colors.primary} />, 0)}
{renderGlassCard('My Purchased Auctions', stats?.myAuctionCount, <ShoppingCart size={24} color={colors.primary} />, 1)}
{renderGlassCard('Cancelled Auctions', stats?.cancelledAuctionCount, <X size={24} color={colors.primary} />, 2)}
{renderGlassCard('Participated Auctions', stats?.currentBiddingCount, <FileText size={24} color={colors.primary} />, 3)}
</>
) : (
<>
{renderGlassCard('Total Purchased', stats[0]?.totalPurchased, <ShoppingCart size={24} color={colors.primary} />, 0)}
{renderGlassCard('Participated', stats[0]?.totalParticipate, <FileText size={24} color={colors.primary} />, 1)}
{renderGlassCard('Total Tenders', stats[0]?.totalTender, <Layers size={24} color={colors.primary} />, 2)}
</>
)}
</View>
);
return (
<View style={styles.screen}>
<Header title="Dashboard" />
<View style={[styles.container, { backgroundColor: colors.background }]}>
<ConfirmDialog
visible={showDialog}
title="Aadhaar Verification Required"
message="To access features, please complete your Aadhaar verification."
confirmText={isPending || isNavigating ? "Processing..." : "Proceed"}
cancelText="Cancel"
onConfirm={handleProceedVerification}
onClose={() => !isPending && !isNavigating && setShowDialog(false)}
loading={isPending || isNavigating}
/>
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
{isNavigating && (
<View style={styles.navigationOverlay}>
<ActivityIndicator size="large" color={colors.primary || '#2563EB'} />
<Text style={styles.loadingText}>Loading verification...</Text>
{/* Large Typography Header */}
<Animated.View entering={FadeInDown.duration(800)} style={styles.headerContainer}>
<View>
<AppText variant="bodySmall" color={colors.textSecondary}>Hello, {userData?.firstName || 'User'}</AppText>
<AppText variant="h1" color={colors.primary} style={{ marginTop: 4 }}>
Find Your{'\n'}Perfect {userType === 'auction' ? 'Deal' : 'Tender'}
</AppText>
</View>
<TouchableOpacity onPress={() => setIsMenuVisible(true)}>
<Menu size={28} color={colors.text} />
</TouchableOpacity>
</Animated.View>
<SidebarModal isVisible={isMenuVisible} onClose={() => setIsMenuVisible(false)} />
{/* Floating/Glass Stats */}
<View style={styles.sectionContainer}>
<AppText variant="button" color={colors.text} style={{ marginBottom: 16 }}>Overview</AppText>
{isLoadingDashboard || isLoadingTender ? (
<ActivityIndicator size="large" color={colors.primary} style={{ marginTop: 20 }} />
) : (
<StatGrid />
)}
</View>
)}
<FlatList
data={DASHBOARD_CARDS}
keyExtractor={(_, index) => index.toString()}
numColumns={2}
columnWrapperStyle={styles.row}
contentContainerStyle={styles.list}
renderItem={({ item, index }) => {
return (
<StatCard
item={item}
index={index}
isLoading={isLoading}
/>
)
}}
showsVerticalScrollIndicator={false}
/>
</View>
{/* Placeholder for Recent Activity list with same glass style */}
<View style={styles.sectionContainer}>
<AppText variant="button" color={colors.text} style={{ marginBottom: 16 }}>Recent Activity</AppText>
<View style={[styles.glassCard, { width: '100%', height: 100, backgroundColor: colors.glass, alignItems: 'center', justifyContent: 'center' }]}>
<AppText variant="body" color={colors.textSecondary}>No recent activity</AppText>
</View>
</View>
</ScrollView>
</View >
);
};
export default Home;
const styles = StyleSheet.create({
screen: {
container: {
flex: 1,
backgroundColor: colors.white,
},
list: {
padding: 16,
paddingBottom: 24,
scrollContent: {
paddingTop: 60,
paddingHorizontal: 20,
paddingBottom: 100, // Space for floating tab bar
},
row: {
headerContainer: {
marginBottom: 40,
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 16,
alignItems: 'flex-start'
},
card: {
width: CARD_WIDTH,
backgroundColor: '#FFFFFF',
borderRadius: 16,
sectionContainer: {
marginBottom: 32,
},
gridContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
glassCard: {
width: (width - 48) / 2, // 2 column grid
padding: 16,
borderRadius: 24,
borderWidth: 1,
borderColor: '#E5E7EB',
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 8,
elevation: 3,
marginBottom: 16,
// Glassmorphism creates a subtle depth without heavy shadows
},
header: {
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 12,
},
title: {
fontSize: 13,
fontWeight: '600',
color: '#374151',
flex: 1,
paddingRight: 8,
},
badge: {
flexDirection: 'row',
iconContainer: {
width: 40,
height: 40,
borderRadius: 12,
alignItems: 'center',
gap: 6,
borderWidth: 1,
borderRadius: 20,
paddingHorizontal: 8,
paddingVertical: 4,
},
badgeText: {
fontSize: 12,
fontWeight: '700',
},
value: {
fontSize: 28,
fontWeight: '800',
color: '#111827',
marginTop: 5,
},
desc: {
fontSize: 12,
color: '#6B7280',
marginTop: 6,
},
navigationOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 999,
},
loadingText: {
marginTop: 12,
fontSize: 16,
cardValue: {
fontSize: 28,
fontWeight: '600',
color: '#374151',
marginBottom: 4,
},
cardLabel: {
fontSize: 14,
fontWeight: '500',
},
});
+28 -14
View File
@@ -32,13 +32,17 @@ export const KycWebViewScreen = ({ route, navigation }: any) => {
};
}, []);
const navigateToLogin = () => {
navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: 'Login' }], // Adjust to your login screen route name
})
);
const handleNavigationBack = () => {
if (data.returnRoute) {
navigation.replace(data.returnRoute, data.returnParams);
} else {
navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: 'Login' }],
})
);
}
};
const startSuccessCountdown = () => {
@@ -49,7 +53,7 @@ export const KycWebViewScreen = ({ route, navigation }: any) => {
const countdownInterval = setInterval(() => {
remainingSeconds -= 1;
setCountdown(remainingSeconds);
if (remainingSeconds <= 0) {
clearInterval(countdownInterval);
}
@@ -57,7 +61,7 @@ export const KycWebViewScreen = ({ route, navigation }: any) => {
successTimeoutRef.current = setTimeout(() => {
clearInterval(countdownInterval);
navigateToLogin();
handleNavigationBack();
}, SUCCESS_DELAY_MS);
};
@@ -77,7 +81,7 @@ export const KycWebViewScreen = ({ route, navigation }: any) => {
const handleNavigationStateChange = (navState: any) => {
if (
navState.title &&
navState.title &&
navState.title.includes('E-KYC SETU - Verification Success') &&
!hasCompletedRef.current
) {
@@ -126,7 +130,17 @@ export const KycWebViewScreen = ({ route, navigation }: any) => {
onLoadStart={handleLoadStart}
onLoadEnd={handleLoadEnd}
onError={handleError}
onNavigationStateChange={handleNavigationStateChange}
onNavigationStateChange={(navState) => {
console.log("Current URL:", navState.url);
handleNavigationStateChange(navState);
}}
onShouldStartLoadWithRequest={(request) => {
console.log("Request URL:", request.url);
return true;
}}
renderLoading={() => <LoadingIndicator />}
/>
)}
@@ -144,9 +158,9 @@ export const KycWebViewScreen = ({ route, navigation }: any) => {
<Text style={styles.countdownText}>
Redirecting in {countdown} seconds...
</Text>
<ActivityIndicator
size="small"
color={colors.primary || '#2563EB'}
<ActivityIndicator
size="small"
color={colors.primary || '#2563EB'}
style={{ marginTop: 16 }}
/>
</View>
+8 -7
View File
@@ -17,6 +17,7 @@ import { useNavigation } from '@react-navigation/native';
import colors from '../../Constant/Color';
import { showToast } from '../../Constant/ShowDailog';
import NavigationString from '../../Constant/NavigationString';
import { BASE_URL } from '../../Services/ApiCall/ApiUrls';
interface ForgotModalProps {
visible: boolean;
@@ -30,7 +31,7 @@ const ForgotModal: React.FC<ForgotModalProps> = ({
visible,
onClose
}) => {
const Navigation = useNavigation<any>();
const Navigation = useNavigation<any>();
const [email, setEmail] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>('');
@@ -58,7 +59,7 @@ const ForgotModal: React.FC<ForgotModalProps> = ({
try {
// Make API call to send OTP for forgot password
const response = await fetch(
`https://procurelinc.in/EProcurementSB/eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`,
`${BASE_URL}eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`,
{
method: 'POST',
headers: {
@@ -71,8 +72,8 @@ const ForgotModal: React.FC<ForgotModalProps> = ({
if (response.ok) {
showToast('OTP has been sent to your email address. Please check your inbox.')
onClose();
Navigation.navigate(NavigationString.ResetPassword, { email });
Navigation.navigate(NavigationString.ResetPassword, { email });
} else {
const errorData = await response.text();
console.error('API Error:', errorData);
@@ -101,13 +102,13 @@ const ForgotModal: React.FC<ForgotModalProps> = ({
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.centeredView}>
<KeyboardAvoidingView
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardAvoidingView}
>
<View style={styles.modalView}>
<Text style={styles.title}>Reset Password</Text>
<Text style={styles.subtitle}>
Enter your email address and we'll send you an OTP to reset your password.
</Text>
@@ -134,7 +135,7 @@ const ForgotModal: React.FC<ForgotModalProps> = ({
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.resetButton, loading && styles.disabledButton]}
onPress={handleResetPassword}
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -263,7 +263,7 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
try {
const res = await axios.post(
`https://procurelinc.in/EProcurementSB/eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`,
`${BASE_URL}eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`,
null,
{
headers: {
@@ -432,7 +432,7 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
)}
</TouchableOpacity>
{!isOtpVerified&&<TouchableOpacity
{!isOtpVerified && <TouchableOpacity
style={[styles.resendButton, resendCooldown > 0 && styles.disabledResendButton]}
onPress={handleResendOtp}
disabled={resendLoading || resendCooldown > 0}
+191 -258
View File
@@ -1,7 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState, useCallback } from 'react';
import {
View,
Text,
ScrollView,
StyleSheet,
TouchableOpacity,
@@ -15,8 +14,11 @@ import {
TextInput,
KeyboardAvoidingView,
Alert,
StatusBar,
Image,
} from 'react-native';
import { LinearGradient } from 'react-native-linear-gradient';
import LinearGradient from 'react-native-linear-gradient';
import Animated, { FadeInDown, FadeInUp, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import {
AlertCircle,
UserCircle,
@@ -51,15 +53,18 @@ import {
Edit,
X,
Save,
ChevronRight,
LogOut
} from 'lucide-react-native';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { useContractorProfile, useUpdateUserProfile, useESetuAdharVarification } from '../../Services/ApiCall/Hooks';
import colors from '../../Constant/Color';
import { useSelector } from 'react-redux';
import ConfirmDialog from '../../Constant/Dialog/ConfirmDialog';
import { generateTransactionId } from '../../Helper/uniqueId';
import NavigationString from '../../Constant/NavigationString';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import ImagePath from '../../Constant/ImagePath';
const { width } = Dimensions.get('window');
@@ -165,11 +170,14 @@ const hasSectionData = (values: any[]): boolean => {
// Main Component
const ProfileTab: React.FC = () => {
const { colors, spacing, shadows, isDarkMode } = useTheme();
const styles = useMemo(() => createStyles(colors), [colors]);
const { data, refetch, isLoading, error } = useContractorProfile();
const user = useSelector((state: any) => state.token?.token);
console.log("User: ", user)
const Navigation: any = useNavigation();
const { mutateAsync: eSetuAdharVarification, isPending } = useESetuAdharVarification();
const [editModalVisible, setEditModalVisible] = useState(false);
const [showDialog, setShowDialog] = useState<boolean>(false);
const [isNavigating, setIsNavigating] = useState<boolean>(false);
@@ -182,19 +190,19 @@ const ProfileTab: React.FC = () => {
const displayMobile = isAuctionUser ? user?.mobileNo : data?.mobileNo;
const handleAadhaarPress = () => {
if (user?.esetuVerified === 'Not Verified') {
if (user?.esetuVerified === 'Not Verified' || user?.esetuVerified === null) {
setShowDialog(true);
}
};
useFocusEffect(()=>{
setIsNavigating(false)
})
useFocusEffect(() => {
setIsNavigating(false)
})
const handleProceedVerification = async () => {
try {
setIsNavigating(true);
const payload = {
contId: user.id,
transactionId: generateTransactionId(),
@@ -202,15 +210,15 @@ const ProfileTab: React.FC = () => {
udf2: "77.34",
udf3: "string",
udf4: "string",
responseUrl: "https://dev.procurelinc.in/"
responseUrl: "http://dev.192.168.6.194:8090/"
};
const res = await eSetuAdharVarification(payload);
setShowDialog(false);
Navigation.navigate(NavigationString.KycWebViewScreen, { res });
} catch (err) {
Alert.alert('Error', 'Failed to initiate verification. Please try again.');
setIsNavigating(false);
@@ -221,7 +229,7 @@ const ProfileTab: React.FC = () => {
return (
<View style={styles.centerContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.loadingText}>Loading profile...</Text>
<AppText style={styles.loadingText}>Loading profile...</AppText>
</View>
);
}
@@ -230,9 +238,9 @@ const ProfileTab: React.FC = () => {
return (
<View style={styles.centerContainer}>
<AlertCircle size={64} color={colors.error} />
<Text style={styles.errorText}>{error}</Text>
<TouchableOpacity style={styles.retryButton} onPress={refetch}>
<Text style={styles.retryButtonText}>Retry</Text>
<AppText style={styles.screenErrorText}>{error instanceof Error ? error.message : String(error)}</AppText>
<TouchableOpacity style={styles.retryButton} onPress={() => refetch()}>
<AppText style={styles.retryButtonText}>Retry</AppText>
</TouchableOpacity>
</View>
);
@@ -243,7 +251,7 @@ const ProfileTab: React.FC = () => {
return (
<>
<ScrollView
style={styles.container}
style={[styles.container, { backgroundColor: colors.background }]}
contentContainerStyle={styles.contentContainer}
refreshControl={
<RefreshControl
@@ -254,11 +262,6 @@ const ProfileTab: React.FC = () => {
/>
}
showsVerticalScrollIndicator={false}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
initialNumToRender={8}
windowSize={10}
>
{/* Header with Gradient */}
<ProfileHeader data={data} displayName={displayName || data.fullName} />
@@ -274,10 +277,10 @@ const ProfileTab: React.FC = () => {
showEdit={!isAuctionUser}
onEdit={() => setEditModalVisible(true)}
>
<InfoRow
label="Full Name"
value={isAuctionUser ? displayName : `${data.firstName} ${data.middleName} ${data.lastName}`.trim()}
IconComponent={User}
<InfoRow
label="Full Name"
value={isAuctionUser ? displayName : `${data.firstName} ${data.middleName} ${data.lastName}`.trim()}
IconComponent={User}
/>
<InfoRow label="Email" value={data.emailId} IconComponent={Mail} />
<InfoRow
@@ -296,14 +299,14 @@ const ProfileTab: React.FC = () => {
data.agentWebsite,
data.agentRegno
]) && (
<InfoSection title="Company Details" IconComponent={Building2} delay={0}>
<InfoRow label="Company" value={data.company} IconComponent={Building} />
<InfoRow label="Type" value={data.contractorType} IconComponent={Tag} />
<InfoRow label="Organization" value={data.contOrganization} IconComponent={Network} />
<InfoRow label="Website" value={data.agentWebsite} IconComponent={Globe} />
<InfoRow label="Registration No" value={data.agentRegno} IconComponent={Award} />
</InfoSection>
)}
<InfoSection title="Company Details" IconComponent={Building2} delay={0}>
<InfoRow label="Company" value={data.company} IconComponent={Building} />
<InfoRow label="Type" value={data.contractorType} IconComponent={Tag} />
<InfoRow label="Organization" value={data.contOrganization} IconComponent={Network} />
<InfoRow label="Website" value={data.agentWebsite} IconComponent={Globe} />
<InfoRow label="Registration No" value={data.agentRegno} IconComponent={Award} />
</InfoSection>
)}
{/* Address Information - Only show if has data */}
{hasSectionData([
@@ -312,13 +315,13 @@ const ProfileTab: React.FC = () => {
data.agentState,
data.agentPincode
]) && (
<InfoSection title="Address Information" IconComponent={MapPin} delay={0}>
<InfoRow label="Address" value={data.agentAddress} IconComponent={Home} />
<InfoRow label="City" value={data.agentCity} IconComponent={Building2} />
<InfoRow label="State" value={data.agentState} IconComponent={Map} />
<InfoRow label="Pincode" value={data.agentPincode} IconComponent={Mailbox} />
</InfoSection>
)}
<InfoSection title="Address Information" IconComponent={MapPin} delay={0}>
<InfoRow label="Address" value={data.agentAddress} IconComponent={Home} />
<InfoRow label="City" value={data.agentCity} IconComponent={Building2} />
<InfoRow label="State" value={data.agentState} IconComponent={Map} />
<InfoRow label="Pincode" value={data.agentPincode} IconComponent={Mailbox} />
</InfoSection>
)}
{/* Tax & Legal - Only show if has data */}
{hasSectionData([
@@ -328,14 +331,14 @@ const ProfileTab: React.FC = () => {
data.gistnNo,
data.comAadharNo
]) && (
<InfoSection title="Tax & Legal Information" IconComponent={FileText} delay={0}>
<InfoRow label="PAN Number" value={data.orgPAN} IconComponent={CreditCard} />
<InfoRow label="PAN Name" value={data.orgPANname} IconComponent={Type} />
<InfoRow label="PAN DOB" value={data.orgPANdob} IconComponent={Calendar} />
<InfoRow label="GSTIN" value={data.gistnNo} IconComponent={Receipt} />
<InfoRow label="Aadhar" value={data.comAadharNo} IconComponent={FileText} />
</InfoSection>
)}
<InfoSection title="Tax & Legal Information" IconComponent={FileText} delay={0}>
<InfoRow label="PAN Number" value={data.orgPAN} IconComponent={CreditCard} />
<InfoRow label="PAN Name" value={data.orgPANname} IconComponent={Type} />
<InfoRow label="PAN DOB" value={data.orgPANdob} IconComponent={Calendar} />
<InfoRow label="GSTIN" value={data.gistnNo} IconComponent={Receipt} />
<InfoRow label="Aadhar" value={data.comAadharNo} IconComponent={FileText} />
</InfoSection>
)}
{/* Registration Status - Only show if has data */}
{hasSectionData([
@@ -344,18 +347,18 @@ const ProfileTab: React.FC = () => {
data.msmeStatus,
data.expiryDate
]) && (
<InfoSection title="Registration Details" IconComponent={ClipboardCheck} delay={0}>
<InfoRow label="Class" value={data.contractorClass} IconComponent={Star} />
<InfoRow label="Category" value={data.socialCategory} IconComponent={List} />
<InfoRow label="MSME Status" value={data.msmeStatus} IconComponent={Factory} />
<InfoRow
label="Expiry Date"
value={data.expiryDate}
IconComponent={CalendarClock}
valueColor={colors.warning}
/>
</InfoSection>
)}
<InfoSection title="Registration Details" IconComponent={ClipboardCheck} delay={0}>
<InfoRow label="Class" value={data.contractorClass} IconComponent={Star} />
<InfoRow label="Category" value={data.socialCategory} IconComponent={List} />
<InfoRow label="MSME Status" value={data.msmeStatus} IconComponent={Factory} />
<InfoRow
label="Expiry Date"
value={data.expiryDate}
IconComponent={CalendarClock}
valueColor={colors.text}
/>
</InfoSection>
)}
<View style={{ height: 40 }} />
</ScrollView>
@@ -401,6 +404,8 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
data,
onSuccess,
}) => {
const { colors } = useTheme();
const styles = useMemo(() => createStyles(colors), [colors]);
const [formData, setFormData] = useState<UpdateProfilePayload>({
firstName: data.firstName || '',
middleName: data.middleName || '',
@@ -484,83 +489,83 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
style={styles.modalOverlay}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={styles.modalContent}>
<View style={[styles.modalContent, { backgroundColor: colors.background }]}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Edit Personal Information</Text>
<AppText variant="h3" style={styles.modalTitle}>Edit Personal Information</AppText>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<X size={24} color={colors.darkText} />
<X size={24} color={colors.text} />
</TouchableOpacity>
</View>
<ScrollView style={styles.formContainer} showsVerticalScrollIndicator={false}>
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>First Name *</Text>
<AppText variant="bodySmall" style={styles.inputLabel}>First Name *</AppText>
<TextInput
style={[styles.input, errors.firstName && styles.inputError]}
value={formData.firstName}
onChangeText={(text) => setFormData({ ...formData, firstName: text })}
placeholder="Enter first name"
placeholderTextColor={colors.grayText}
placeholderTextColor={colors.textSecondary}
/>
{errors.firstName && (
<Text style={styles.errorText}>{errors.firstName}</Text>
<AppText variant="caption" style={styles.errorText}>{errors.firstName}</AppText>
)}
</View>
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Middle Name</Text>
<AppText variant="bodySmall" style={styles.inputLabel}>Middle Name</AppText>
<TextInput
style={styles.input}
value={formData.middleName}
onChangeText={(text) => setFormData({ ...formData, middleName: text })}
placeholder="Enter middle name"
placeholderTextColor={colors.grayText}
placeholderTextColor={colors.textSecondary}
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Last Name *</Text>
<AppText variant="bodySmall" style={styles.inputLabel}>Last Name *</AppText>
<TextInput
style={[styles.input, errors.lastName && styles.inputError]}
value={formData.lastName}
onChangeText={(text) => setFormData({ ...formData, lastName: text })}
placeholder="Enter last name"
placeholderTextColor={colors.grayText}
placeholderTextColor={colors.textSecondary}
/>
{errors.lastName && (
<Text style={styles.errorText}>{errors.lastName}</Text>
<AppText variant="caption" style={styles.errorText}>{errors.lastName}</AppText>
)}
</View>
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Email *</Text>
<AppText variant="bodySmall" style={styles.inputLabel}>Email *</AppText>
<TextInput
style={[styles.input, errors.emailId && styles.inputError]}
value={formData.emailId}
onChangeText={(text) => setFormData({ ...formData, emailId: text })}
placeholder="Enter email"
placeholderTextColor={colors.grayText}
placeholderTextColor={colors.textSecondary}
keyboardType="email-address"
autoCapitalize="none"
/>
{errors.emailId && (
<Text style={styles.errorText}>{errors.emailId}</Text>
<AppText variant="caption" style={styles.errorText}>{errors.emailId}</AppText>
)}
</View>
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Mobile Number *</Text>
<AppText variant="bodySmall" style={styles.inputLabel}>Mobile Number *</AppText>
<TextInput
style={[styles.input, errors.mobileNo && styles.inputError]}
value={formData.mobileNo}
onChangeText={(text) => setFormData({ ...formData, mobileNo: text })}
placeholder="Enter mobile number"
placeholderTextColor={colors.grayText}
placeholderTextColor={colors.textSecondary}
keyboardType="phone-pad"
maxLength={10}
/>
{errors.mobileNo && (
<Text style={styles.errorText}>{errors.mobileNo}</Text>
<AppText variant="caption" style={styles.errorText}>{errors.mobileNo}</AppText>
)}
</View>
</ScrollView>
@@ -571,7 +576,7 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
onPress={onClose}
disabled={updateProfileMutation.isPending}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
<AppText variant="button" style={styles.cancelButtonText}>Cancel</AppText>
</TouchableOpacity>
<TouchableOpacity
@@ -580,11 +585,11 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
disabled={updateProfileMutation.isPending}
>
{updateProfileMutation.isPending ? (
<ActivityIndicator size="small" color={colors.white} />
<ActivityIndicator size="small" color={colors.primary} />
) : (
<>
<Save size={18} color={colors.white} />
<Text style={styles.saveButtonText}>Save</Text>
<Save size={18} color={colors.buttonText} />
<AppText variant="button" style={styles.saveButtonText}>Save</AppText>
</>
)}
</TouchableOpacity>
@@ -597,6 +602,9 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
// Profile Header Component - OPTIMIZED
const ProfileHeader: React.FC<ProfileHeaderProps> = React.memo(({ data, displayName }) => {
const { colors } = useTheme();
const styles = useMemo(() => createStyles(colors), [colors]);
const getInitials = (name: string): string => {
if (!name || typeof name !== 'string' || name.trim().length === 0) {
const fallbackName = `${data.firstName || ''} ${data.lastName || ''}`.trim();
@@ -630,46 +638,44 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = React.memo(({ data, displayN
return (
<View>
<LinearGradient
colors={[colors.primary, colors.primaryLight]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
<View
style={styles.headerGradient}
>
<View style={styles.avatarContainer}>
<View style={styles.avatar}>
<Text style={styles.avatarText}>{getInitials(displayName)}</Text>
<AppText variant="h1" color={colors.text} style={styles.avatarText}>{getInitials(displayName)}</AppText>
</View>
<View style={[styles.statusBadge, { backgroundColor: getStatusColor() }]}>
<View style={styles.statusDot} />
</View>
</View>
<Text style={styles.headerName}>
<AppText variant="h2" style={styles.headerName}>
{displayName || 'Unknown User'}
</Text>
</AppText>
<View style={styles.roleBadge}>
<ShieldCheck size={16} color={colors.white} />
<Text style={styles.roleText}>{data.role}</Text>
<ShieldCheck size={16} color={colors.text} />
<AppText variant="caption" style={styles.roleText}>{data.role}</AppText>
</View>
</LinearGradient>
</View>
</View>
);
});
ProfileHeader.displayName = 'ProfileHeader';
// Quick Stats Component - OPTIMIZED WITH TOUCHABLE AADHAAR
// Quick Stats Component - Glass Cards
const QuickStats: React.FC<QuickStatsProps> = React.memo(({ data, onAadhaarPress }) => {
const user = useSelector((state: any) => state.token?.token);
const { colors } = useTheme();
const stats: StatItem[] = useMemo(
() => [
{
Icon: CheckCircle,
label: 'Aadhaar',
value: user?.esetuVerified,
value: user?.esetuVerified || 'Pending',
color: user?.esetuVerified === 'Not Verified' ? colors.error : colors.success,
onPress: onAadhaarPress,
},
@@ -677,169 +683,103 @@ const QuickStats: React.FC<QuickStatsProps> = React.memo(({ data, onAadhaarPress
Icon: ShieldCheck,
label: '2FA',
value: data.twoFactorAuthEnabled ? 'Enabled' : 'Disabled',
color: data.twoFactorAuthEnabled ? colors.success : colors.grayText,
},
{
Icon: FileText,
label: 'Registration',
value: data.registrationStatus,
color: colors.info,
color: data.twoFactorAuthEnabled ? colors.success : colors.textSecondary,
},
// ...
],
[data.twoFactorAuthEnabled, data.registrationStatus, user?.esetuVerified, onAadhaarPress]
[data.twoFactorAuthEnabled, user?.esetuVerified, onAadhaarPress, colors]
);
return (
<View style={styles.statsContainer}>
{stats.map((stat) => {
const CardContent = (
<View style={styles.statCard}>
<stat.Icon size={28} color={stat.color} />
<Text style={styles.statValue} numberOfLines={2}>
{stat.value}
</Text>
<Text style={styles.statLabel}>{stat.label}</Text>
</View>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 24 }}>
{stats.map((stat, index) => {
const content = (
<Animated.View
entering={FadeInUp.duration(600).delay(index * 100)}
style={{
flex: 1, backgroundColor: colors.glass, borderColor: colors.overlay, borderWidth: 1,
borderRadius: 20, padding: 12, alignItems: 'center', marginHorizontal: 4,
minHeight: 100, justifyContent: 'center'
}}
>
<View style={{ width: 40, height: 40, borderRadius: 20, backgroundColor: stat.color + '10', alignItems: 'center', justifyContent: 'center', marginBottom: 8 }}>
<stat.Icon size={20} color={stat.color} />
</View>
<AppText variant="bodySmall" color={colors.text} numberOfLines={1} style={{ textAlign: 'center' }}>{stat.value}</AppText>
<AppText variant="caption" color={colors.textSecondary}>{stat.label}</AppText>
</Animated.View>
);
if (stat.onPress) {
return (
<TouchableOpacity
key={stat.label}
activeOpacity={0.7}
onPress={stat.onPress}
style={{ flex: 1 }}
>
{CardContent}
</TouchableOpacity>
);
}
return <View key={stat.label} style={{ flex: 1 }}>{CardContent}</View>;
return stat.onPress ? (
<TouchableOpacity key={stat.label} style={{ flex: 1 }} onPress={stat.onPress}>{content}</TouchableOpacity>
) : (
<View key={stat.label} style={{ flex: 1 }}>{content}</View>
);
})}
</View>
);
});
QuickStats.displayName = 'QuickStats';
// Info Section Component - OPTIMIZED
// Info Section Component - Glass Card Container
const InfoSection: React.FC<InfoSectionProps> = React.memo(
({ title, IconComponent, children, onEdit, showEdit = false }) => {
({ title, IconComponent, children, onEdit, showEdit = false, delay = 0 }) => {
const { colors } = useTheme();
return (
<View style={styles.section}>
<View style={styles.sectionHeader}>
<View style={styles.sectionHeaderLeft}>
<View style={styles.sectionIconContainer}>
<IconComponent size={24} color={colors.primary} />
<Animated.View
entering={FadeInUp.duration(600).delay(delay)}
style={{
backgroundColor: colors.glass,
borderColor: colors.overlay,
borderWidth: 1,
borderRadius: 24,
padding: 20,
marginBottom: 16
}}
>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={{ padding: 8, backgroundColor: colors.primary + '10', borderRadius: 10, marginRight: 12 }}>
<IconComponent size={20} color={colors.primary} />
</View>
<Text style={styles.sectionTitle}>{title}</Text>
<AppText variant="h4" color={colors.text}>{title}</AppText>
</View>
{showEdit && onEdit && (
<TouchableOpacity style={styles.editButton} onPress={onEdit}>
<Edit size={20} color={colors.primary} />
<TouchableOpacity onPress={onEdit} style={{ padding: 4 }}>
<Edit size={18} color={colors.primary} />
</TouchableOpacity>
)}
</View>
<View style={styles.sectionContent}>{children}</View>
</View>
<View>{children}</View>
</Animated.View>
);
}
);
InfoSection.displayName = 'InfoSection';
// Info Row Component - OPTIMIZED
// Info Row Component
const InfoRow: React.FC<InfoRowProps> = React.memo(
({ label, value, IconComponent, valueColor }) => {
if (!isValidValue(value)) {
return null;
}
const { colors } = useTheme();
if (!isValidValue(value)) return null;
return (
<View style={styles.infoRow}>
<View style={styles.infoRowTouchable}>
<View style={styles.infoRowContent}>
<View style={styles.infoIconContainer}>
<IconComponent size={18} color={colors.primary} />
</View>
<View style={styles.infoTextContainer}>
<Text style={styles.infoLabel}>{label}</Text>
<Text
style={[styles.infoValue, valueColor && { color: valueColor }]}
>
{value}
</Text>
</View>
</View>
<View style={{ flexDirection: 'row', marginBottom: 16, alignItems: 'flex-start' }}>
<IconComponent size={16} color={colors.textSecondary} style={{ marginTop: 4, marginRight: 12 }} />
<View style={{ flex: 1 }}>
<AppText variant="caption" color={colors.textSecondary} style={{ marginBottom: 2 }}>{label}</AppText>
<AppText variant='bodySmall' color={valueColor || colors.text}>{value}</AppText>
</View>
</View>
);
}
);
InfoRow.displayName = 'InfoRow';
// Styles
interface Styles {
container: ViewStyle;
contentContainer: ViewStyle;
centerContainer: ViewStyle;
loadingText: TextStyle;
errorText: TextStyle;
retryButton: ViewStyle;
retryButtonText: TextStyle;
headerGradient: ViewStyle;
avatarContainer: ViewStyle;
avatar: ViewStyle;
avatarText: TextStyle;
statusBadge: ViewStyle;
statusDot: ViewStyle;
headerName: TextStyle;
roleBadge: ViewStyle;
roleText: TextStyle;
statsContainer: ViewStyle;
statCard: ViewStyle;
statValue: TextStyle;
statLabel: TextStyle;
section: ViewStyle;
sectionHeader: ViewStyle;
sectionHeaderLeft: ViewStyle;
sectionIconContainer: ViewStyle;
sectionTitle: TextStyle;
sectionContent: ViewStyle;
infoRow: ViewStyle;
infoRowTouchable: ViewStyle;
infoRowContent: ViewStyle;
infoIconContainer: ViewStyle;
infoTextContainer: ViewStyle;
infoLabel: TextStyle;
infoValue: TextStyle;
editButton: ViewStyle;
modalOverlay: ViewStyle;
modalContent: ViewStyle;
modalHeader: ViewStyle;
modalTitle: TextStyle;
closeButton: ViewStyle;
formContainer: ViewStyle;
inputGroup: ViewStyle;
inputLabel: TextStyle;
input: TextStyle;
inputError: TextStyle;
errorText: TextStyle;
modalFooter: ViewStyle;
button: ViewStyle;
cancelButton: ViewStyle;
saveButton: ViewStyle;
cancelButtonText: TextStyle;
saveButtonText: TextStyle;
navigationOverlay: ViewStyle;
}
const styles = StyleSheet.create<Styles>({
const createStyles = (colors: any) => StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.lightGray,
paddingHorizontal: 20
},
contentContainer: {
paddingBottom: 20,
@@ -848,16 +788,16 @@ const styles = StyleSheet.create<Styles>({
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.white,
backgroundColor: colors.background,
padding: 20,
},
loadingText: {
marginTop: 16,
fontSize: 16,
color: colors.grayText,
color: colors.textSecondary,
fontWeight: '500',
},
errorText: {
screenErrorText: {
marginTop: 16,
fontSize: 16,
color: colors.error,
@@ -886,13 +826,6 @@ const styles = StyleSheet.create<Styles>({
paddingTop: Platform.OS === 'ios' ? 60 : 40,
paddingBottom: 30,
alignItems: 'center',
borderBottomLeftRadius: 32,
borderBottomRightRadius: 32,
elevation: 8,
shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
},
avatarContainer: {
position: 'relative',
@@ -902,11 +835,11 @@ const styles = StyleSheet.create<Styles>({
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: colors.white,
backgroundColor: colors.card,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 4,
borderColor: colors.white,
borderColor: colors.card,
elevation: 4,
shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 2 },
@@ -926,7 +859,7 @@ const styles = StyleSheet.create<Styles>({
height: 24,
borderRadius: 12,
borderWidth: 3,
borderColor: colors.white,
borderColor: colors.card, // Updated
justifyContent: 'center',
alignItems: 'center',
},
@@ -939,7 +872,7 @@ const styles = StyleSheet.create<Styles>({
headerName: {
fontSize: 26,
fontWeight: '700',
color: colors.white,
color: colors.text, // Updated
marginBottom: 8,
textAlign: 'center',
paddingHorizontal: 20,
@@ -947,14 +880,14 @@ const styles = StyleSheet.create<Styles>({
roleBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.25)',
backgroundColor: colors.primary + '20', // Updated opacity approach
paddingHorizontal: 16,
paddingVertical: 6,
borderRadius: 20,
gap: 6,
},
roleText: {
color: colors.white,
color: colors.primary, // Updated
fontSize: 14,
fontWeight: '600',
},
@@ -968,7 +901,7 @@ const styles = StyleSheet.create<Styles>({
},
statCard: {
flex: 1,
backgroundColor: colors.white,
backgroundColor: colors.card,
borderRadius: 16,
padding: 16,
alignItems: 'center',
@@ -981,20 +914,20 @@ const styles = StyleSheet.create<Styles>({
statValue: {
fontSize: 14,
fontWeight: '700',
color: colors.darkText,
color: colors.text,
marginTop: 8,
textAlign: 'center',
},
statLabel: {
fontSize: 12,
color: colors.grayText,
color: colors.textSecondary,
marginTop: 4,
},
section: {
marginHorizontal: 16,
marginTop: 24,
backgroundColor: colors.white,
backgroundColor: colors.card,
borderRadius: 20,
padding: 20,
elevation: 2,
@@ -1021,7 +954,7 @@ const styles = StyleSheet.create<Styles>({
width: 40,
height: 40,
borderRadius: 12,
backgroundColor: colors.primaryHighlight,
backgroundColor: colors.primary + '10', // Updated
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
@@ -1029,7 +962,7 @@ const styles = StyleSheet.create<Styles>({
sectionTitle: {
fontSize: 18,
fontWeight: '700',
color: colors.darkText,
color: colors.text,
flex: 1,
},
sectionContent: {
@@ -1038,7 +971,7 @@ const styles = StyleSheet.create<Styles>({
editButton: {
padding: 8,
borderRadius: 8,
backgroundColor: colors.primaryHighlight,
backgroundColor: colors.primary + '10', // Updated
},
infoRow: {
@@ -1047,7 +980,7 @@ const styles = StyleSheet.create<Styles>({
},
infoRowTouchable: {
padding: 14,
backgroundColor: colors.lightestGray,
backgroundColor: colors.background, // Updated
borderRadius: 12,
},
infoRowContent: {
@@ -1058,7 +991,7 @@ const styles = StyleSheet.create<Styles>({
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: colors.white,
backgroundColor: colors.card, // Updated
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
@@ -1070,13 +1003,13 @@ const styles = StyleSheet.create<Styles>({
},
infoLabel: {
fontSize: 13,
color: colors.grayText,
color: colors.textSecondary,
fontWeight: '500',
lineHeight: 18,
},
infoValue: {
fontSize: 15,
color: colors.darkText,
color: colors.text,
fontWeight: '600',
lineHeight: 21,
},
@@ -1087,11 +1020,11 @@ const styles = StyleSheet.create<Styles>({
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: colors.white,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
maxHeight: '90%',
paddingTop: 20,
borderRadius: 30
},
modalHeader: {
flexDirection: 'row',
@@ -1105,7 +1038,7 @@ const styles = StyleSheet.create<Styles>({
modalTitle: {
fontSize: 20,
fontWeight: '700',
color: colors.darkText,
color: colors.text,
},
closeButton: {
padding: 4,
@@ -1120,7 +1053,7 @@ const styles = StyleSheet.create<Styles>({
inputLabel: {
fontSize: 14,
fontWeight: '600',
color: colors.darkText,
color: colors.text,
marginBottom: 8,
},
input: {
@@ -1129,8 +1062,8 @@ const styles = StyleSheet.create<Styles>({
borderRadius: 12,
padding: 14,
fontSize: 15,
color: colors.darkText,
backgroundColor: colors.white,
color: colors.text,
backgroundColor: colors.card, // Updated
},
inputError: {
borderColor: colors.error,
@@ -1157,7 +1090,7 @@ const styles = StyleSheet.create<Styles>({
gap: 8,
},
cancelButton: {
backgroundColor: colors.lightestGray,
backgroundColor: colors.background, // Updated
},
saveButton: {
backgroundColor: colors.primary,
@@ -1165,7 +1098,7 @@ const styles = StyleSheet.create<Styles>({
cancelButtonText: {
fontSize: 16,
fontWeight: '600',
color: colors.darkText,
color: colors.text,
},
saveButtonText: {
fontSize: 16,
+110 -84
View File
@@ -3,23 +3,22 @@ import {
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
Platform,
} from 'react-native';
import React, { use, useState } from 'react';
import { useNavigation, useRoute, NavigationProp, RouteProp } from '@react-navigation/native';
import colors from '../../Constant/Color';
import React, { useMemo, useState } from 'react';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
import MyInput from '../../Component/MyInput';
import MyInput from '../../components/MyInput';
import { showToast } from '../../Constant/ShowDailog';
import { PARTNER_API_ENDPOINT } from '../../Services/ApiCall/ApiUrls';
import { usePartnerShipSubmit } from '../../Services/ApiCall/Hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Navigation } from 'lucide-react-native';
import { NavigationString } from '../../Constant/AllImports';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import { Trash2, UserPlus } from 'lucide-react-native';
type Partner = {
partnerName: string;
@@ -32,11 +31,12 @@ type Navigation = {
replace: (route: string) => void;
};
type PartnershipParams = {
otherId: string | number
otherId: string | number;
};
const Partnership = () => {
const route = useRoute<RouteProp<{ params: PartnershipParams }, 'params'>>();
const { colors, spacing, typography, shadows } = useTheme();
const navigation = useNavigation() as Navigation;
const [partners, setPartners] = useState<Partner[]>([
{
@@ -49,9 +49,11 @@ const Partnership = () => {
]);
const [focusedField, setFocusedField] = useState<string>('');
const [loading, setLoading] = useState(false)
const { mutateAsync:partnerShipSubmit } = usePartnerShipSubmit();
const insets = useSafeAreaInsets();
const [loading, setLoading] = useState(false);
const { mutateAsync: partnerShipSubmit } = usePartnerShipSubmit();
const insets = useSafeAreaInsets();
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
const handleChange = (index: number, name: keyof Partner, value: string) => {
const updated = [...partners];
updated[index][name] = value;
@@ -90,9 +92,8 @@ const Partnership = () => {
};
const handleSubmit = async () => {
setLoading(true)
setLoading(true);
try {
// Convert percentage strings to numbers and validate
const payload = partners.map((p, i) => {
const percentage = Number(p.percentage);
@@ -105,13 +106,12 @@ const Partnership = () => {
ptnrAadharNo: p.aadharNumber,
ptnrMobNo: p.mobileNumber,
ptnrName: p.partnerName,
ptnrPop: percentage, // Ensure this is a number
ptnrPop: percentage,
ptnrPANNo: p.partnerPan,
ptnrNo: i,
};
});
// Validate total percentage equals 100%
const totalPercentage = payload.reduce((sum, p) => sum + p.ptnrPop, 0);
if (totalPercentage !== 100) {
showToast(`Total percentage must equal 100% (currently ${totalPercentage}%)`);
@@ -123,153 +123,179 @@ const Partnership = () => {
if (response?.statusCode === 200) {
showToast(response.message || 'Submitted successfully!');
navigation.replace(NavigationString.Home);
}
} catch (error: any) {
console.error('❌ Submission failed:', error.message);
showToast(error.message || 'Submission failed. Please check partner details.');
}finally{
setLoading(false)
} finally {
setLoading(false);
}
};
return (
<View style={{ flex: 1 }}>
<Text style={[styles.heading, { paddingTop: insets.top }]}>Partnership Details</Text>
<View style={{ flex: 1, backgroundColor: colors.background }}>
<View style={[styles.header, { paddingTop: insets.top + spacing.s }]}>
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700' }}>
Partnership Details
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
Add all partners and their share percentages
</AppText>
</View>
<SafeAreaView style={{ flex: 1 }}>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40}
>
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled">
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{partners.map((partner, index) => (
<View style={{ marginBottom: 20 }} key={index}>
<Text style={styles.partnerTitle}>Partner {index + 1}</Text>
<Animated.View
key={index}
entering={FadeInUp.delay(index * 80).springify()}
style={[styles.card, shadows.medium]}
>
<View style={styles.cardHeader}>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
Partner {index + 1}
</AppText>
{partners.length > 1 && (
<TouchableOpacity onPress={() => handleRemove(index)} activeOpacity={0.7}>
<Trash2 size={18} color={colors.error} />
</TouchableOpacity>
)}
</View>
<MyInput
icon="user"
placeholder="Name of Partner *"
label="Partner Name"
placeholder="Enter Partner Name"
value={partner.partnerName}
onChangeText={text => handleChange(index, 'partnerName', text)}
onFocus={() => setFocusedField(`partnerName-${index}`)}
onBlur={() => setFocusedField('')}
focused={focusedField === `partnerName-${index}`}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="percent"
placeholder="Partnership Percentage *"
label="Share Percentage"
placeholder="Enter Percentage"
value={partner.percentage}
onChangeText={text => handleChange(index, 'percentage', text)}
onFocus={() => setFocusedField(`percentage-${index}`)}
onBlur={() => setFocusedField('')}
focused={focusedField === `percentage-${index}`}
keyboardType="numeric"
maxlength={3}
maxLength={3}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="id-card"
placeholder="PAN of Partner *"
label="PAN of Partner"
placeholder="Enter PAN Number"
value={partner.partnerPan}
onChangeText={text => handleChange(index, 'partnerPan', text)}
onFocus={() => setFocusedField(`partnerPan-${index}`)}
onBlur={() => setFocusedField('')}
focused={focusedField === `partnerPan-${index}`}
autoCapitalize="characters"
maxlength={10}
maxLength={10}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="credit-card"
placeholder="Aadhar Number *"
label="Aadhaar Number"
placeholder="Enter 12-digit Aadhaar"
value={partner.aadharNumber}
onChangeText={text => handleChange(index, 'aadharNumber', text)}
onFocus={() => setFocusedField(`aadharNumber-${index}`)}
onBlur={() => setFocusedField('')}
focused={focusedField === `aadharNumber-${index}`}
keyboardType="numeric"
maxlength={12}
maxLength={12}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="phone"
placeholder="Mobile Number *"
label="Mobile Number"
placeholder="Enter Mobile Number"
value={partner.mobileNumber}
onChangeText={text => handleChange(index, 'mobileNumber', text)}
onFocus={() => setFocusedField(`mobileNumber-${index}`)}
onBlur={() => setFocusedField('')}
focused={focusedField === `mobileNumber-${index}`}
keyboardType="phone-pad"
maxlength={10}
maxLength={10}
/>
{partners.length > 1 && (
<TouchableOpacity style={styles.deleteButton} onPress={() => handleRemove(index)}>
<Text style={styles.deleteText}>Delete Partner</Text>
</TouchableOpacity>
)}
</View>
</Animated.View>
))}
<TouchableOpacity style={styles.addButton} onPress={handleAddMore}>
<Text style={styles.addText}>+ Add Partner</Text>
<TouchableOpacity style={styles.addButton} onPress={handleAddMore} activeOpacity={0.8}>
<UserPlus size={16} color={colors.buttonText} />
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700', marginLeft: 8 }}>
Add Partner
</AppText>
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
<PrimaryButton title={loading ? "Loading..." : "Submit" }onPress={handleSubmit} />
<View style={styles.footer}>
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} />
</View>
</View>
);
};
export default Partnership;
const styles = StyleSheet.create({
heading: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
marginTop: 10,
textAlign: 'center',
},
partnerTitle: {
fontWeight: '600',
fontSize: 16,
marginBottom: 10,
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
header: {
paddingHorizontal: spacing.m,
paddingBottom: spacing.m,
borderBottomWidth: 1,
borderBottomColor: colors.border,
backgroundColor: colors.surface,
},
scrollContainer: {
// backgroundColor: color.background,
flexGrow: 1,
padding: spacing.m,
paddingBottom: 30,
},
card: {
backgroundColor: colors.surface,
borderRadius: 14,
padding: 14,
marginBottom: spacing.m,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.s,
paddingBottom: spacing.xs,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
},
addButton: {
backgroundColor: colors.primary,
padding: 10,
marginTop: 10,
borderRadius: 10,
flexDirection: 'row',
alignItems: 'center',
width: 140,
justifyContent: 'center',
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 12,
alignSelf: 'flex-end',
},
addText: {
color: 'white',
fontWeight: '600',
},
deleteButton: {
backgroundColor: '#FF5C5C',
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 140,
alignSelf: 'flex-end',
},
deleteText: {
color: 'white',
fontWeight: '600',
footer: {
padding: spacing.m,
borderTopWidth: 1,
borderTopColor: colors.border,
backgroundColor: colors.surface,
},
});
+181 -162
View File
@@ -1,24 +1,16 @@
import React, { useEffect, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Platform, KeyboardAvoidingView, SafeAreaView, ScrollView, BackHandler, ToastAndroid, Button, Alert } from 'react-native';
// import AntDesign from 'react-native-vector-icons/AntDesign';
import React, { useEffect, useMemo, useState } from 'react';
import { View, TouchableOpacity, StyleSheet, Platform, KeyboardAvoidingView, ScrollView, BackHandler, ToastAndroid, Alert } from 'react-native';
import { ArrowLeft } from 'lucide-react-native';
import { NavigationProp, RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import AsyncStorage from '@react-native-async-storage/async-storage';
// import { sendAadhaarOtp, verifyAadhaarOtp, verifyGstNumber, verifyPanHandlerRes } from '../../utils/helper/ragisterationApiCalls';
// import { getAllMsMeOrg, getCityDropdown, getClassDropdown, getMsmeDropdown, getOrgDropdown, getSocialDropdown, getStateDropdown } from '../../utils/helper/dropdownApis';
import { ApiResponse, DocumentsType, FormDataType, OrgType, UserData } from './types/types';
import { pick } from '@react-native-documents/picker';
// import { REJ_STAGE_TWO } from '../../utils/services/apiUrl';
// import LoaderModel from '../../component/LoaderModal';
import axios from 'axios';
import StageOne from './ragistrationStages/StageOne';
import StageTwo from './ragistrationStages/StageTwo';
import StageZero from './ragistrationStages/StageZero';
import StageThree from './ragistrationStages/StageThree';
import StageFour from './ragistrationStages/StageFour';
import colors from '../../Constant/Color';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
import StepProgressBar from '../../Component/StepProgressBar';
import { formatDateInput } from '../../Helper/formatDateInput';
@@ -26,6 +18,9 @@ import { showToast } from '../../Constant/ShowDailog';
import { useCityDropdown, useContractorStageTwoSubmit, usefetchMasterData, useSendAadhaarOtp, useVerifyAadhaarOtp, useVerifyGstNumber, useVerifyPan } from '../../Services/ApiCall/Hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useSelector } from 'react-redux';
import Animated, { FadeInUp, FadeInDown } from 'react-native-reanimated';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
type RouteParams = {
userData?: {
@@ -38,11 +33,12 @@ type RouteParams = {
};
const Registration: React.FC = () => {
const { colors, spacing, typography, shadows } = useTheme();
const navigation = useNavigation<any>();
const { params } = useRoute<RouteProp<Record<string, RouteParams>, string>>();
const userData = params as RouteParams;
const [stage, setStage] = useState(0);
const insets = useSafeAreaInsets();
const insets = useSafeAreaInsets();
const [focusedField, setFocusedField] = useState<null | string>(null);
const [selectedOrg, setSelectedOrg] = useState<OrgType>(0);
const [documents, setDocuments] = useState<DocumentsType>({
@@ -102,58 +98,52 @@ const Registration: React.FC = () => {
const { mutateAsync: verifyGstNumber } = useVerifyGstNumber();
const { mutateAsync: contractorStageTwoSubmit } = useContractorStageTwoSubmit();
const { data: masterData } = usefetchMasterData();
useEffect(() => {
if (!masterData?.data) return;
const contractorClasses = masterData.data.contractorClasses || [];
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
useEffect(() => {
if (!masterData?.data) return;
const contractorClasses = masterData.data.contractorClasses || [];
const msmeOrgs = masterData.data.msmeOrgs || [];
const mappedmsmeOrgs = msmeOrgs.map((item:any)=> ({
label: item.comName,
value: item.msmeId,
}));
const orgs = masterData.data.organizationTypes || [];
const mappedcontractorClasses = contractorClasses.map((item:any)=> ({
label: item.contClassName || '',
value: item.contClassId,
}));
const mapped = orgs.map((item:any)=> ({
label: item.organizationType,
value: item.orgId,
}));
const socialCategories = masterData.data.socialCategories || [];
const mappedsocialCategories = socialCategories.map((item:any)=> ({
label: item.categoryName,
value: item.socialCategoryId,
}));
const stateList = masterData.data.states || [];
const mappedStates = stateList.map((item:any)=> ({
label: item.stateName,
value: item.stateId,
}));
setAllMsmeOrgs(mappedmsmeOrgs);
setAllSocialCategories(mappedsocialCategories);
setContractorClassItems(mappedcontractorClasses);
setAllOrganizations(mapped);
setstates(mappedStates);
}, [masterData?.data]);
const mappedmsmeOrgs = msmeOrgs.map((item: any) => ({
label: item.comName,
value: item.msmeId,
}));
const orgs = masterData.data.organizationTypes || [];
const mappedcontractorClasses = contractorClasses.map((item: any) => ({
label: item.contClassName || '',
value: item.contClassId,
}));
const mapped = orgs.map((item: any) => ({
label: item.organizationType,
value: item.orgId,
}));
const socialCategories = masterData.data.socialCategories || [];
const mappedsocialCategories = socialCategories.map((item: any) => ({
label: item.categoryName,
value: item.socialCategoryId,
}));
const stateList = masterData.data.states || [];
const mappedStates = stateList.map((item: any) => ({
label: item.stateName,
value: item.stateId,
}));
setAllMsmeOrgs(mappedmsmeOrgs);
setAllSocialCategories(mappedsocialCategories);
setContractorClassItems(mappedcontractorClasses);
setAllOrganizations(mapped);
setstates(mappedStates);
}, [masterData?.data]);
const { mutate: cityDropdownMutate } = useCityDropdown();
const { data: MasterData}=usefetchMasterData()
const user = useSelector((state: any) => state.token.token);
const { data: MasterData } = usefetchMasterData();
const user = useSelector((state: any) => state.token.token);
const steps = ['', '', '', '', ''];
const dropdownStyle = {
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
backgroundColor: '#fff',
marginBottom: 10,
}
useEffect(() => {
const fetchUserData = async (user:any) => {
const fetchUserData = async (user: any) => {
try {
if (user) {
const parsedUser = user;
setFormData(prev => ({
@@ -180,25 +170,11 @@ useEffect(() => {
gstin = gstin.toUpperCase();
const panRegex = /^[A-Z]{5}[0-9]{4}[A-Z]$/;
const gstinRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/;
if (!panRegex.test(pan)) {
return { valid: false, message: "Invalid PAN format. It should be like 'ABCDE1234F'." };
}
// if (!gstinRegex.test(gstin)) {
// return { valid: false, message: "Invalid GSTIN format. It should be like '22ABCDE1234F1Z5'." };
// }
// const gstinPAN = gstin.slice(2, 12); // Extract PAN part from GSTIN
// if (pan !== gstinPAN) {
// return {
// valid: false,
// message: "PAN does not match the PAN embedded in GSTIN.",
// };
// }
return { valid: true, message: 'PAN and GSTIN are correctly linked.' };
};
@@ -207,7 +183,7 @@ useEffect(() => {
};
const verifyPanHandler = async () => {
setPanVerifying(true); // Start loading
setPanVerifying(true);
const payload = {
dob: formData?.dateOfIncorp.trim(),
name: formData?.orgPanName.trim(),
@@ -220,7 +196,7 @@ useEffect(() => {
} else {
setPanVerified(false);
}
setPanVerifying(false); // Stop loading
setPanVerifying(false);
};
const sendAdharOTPHandler = async (text: string) => {
@@ -300,7 +276,7 @@ useEffect(() => {
const handleSubmit = async () => {
setLoading(true);
const dtoData = {
agentAddress: formData.address,
agentCity: formData.city,
@@ -320,7 +296,7 @@ useEffect(() => {
orgPAN: formData.orgPanNumber,
comAadharNo: formData.aadhaarNumber,
gistnNo: formData.gstnNo,
stateName: MasterData?.data?.states?.find((state:any)=>state.stateId==formData.state)?.stateName || '',
stateName: MasterData?.data?.states?.find((state: any) => state.stateId == formData.state)?.stateName || '',
};
try {
@@ -333,13 +309,12 @@ useEffect(() => {
fileData.append('pancardfile', documents.panCard as any);
fileData.append('regfile', documents.registrationCertificate as any);
const res = await contractorStageTwoSubmit(fileData);
if (res.status === 200) {
if (res.statusCode === 200) {
if (res) {
showToast('Form submitted successfully!');
const orgType = selectedOrg;
const params = { otherId: res?.data?.contOtherId, orgType: orgType };
if ([4, 5, 6].includes(orgType)) {
@@ -357,7 +332,6 @@ useEffect(() => {
}
} catch (error: any) {
if (axios.isAxiosError(error)) {
} else {
console.error('Unexpected error', error);
}
@@ -369,24 +343,20 @@ useEffect(() => {
};
const fetchCity = async (stateId: number) => {
cityDropdownMutate(stateId, {
onSuccess: (data) => {
setCities(data);
},
onError: (error) => {
setCities([]);
console.error('Error in city fetch:', error);
}
onSuccess: (data) => {
setCities(data);
},
onError: (error) => {
setCities([]);
console.error('Error in city fetch:', error);
}
});
};
console.log('formdata : ', formData);
useEffect(() => {
if (formData?.state) {
fetchCity(Number(formData.state));
@@ -502,15 +472,31 @@ useEffect(() => {
}
};
// ── Stage Titles ──
const stageTitles = [
'Personal & Contact',
'Registration Details',
'KYC Verification',
'Address Details',
'Upload Documents',
];
return (
<View style={{ flex: 1, backgroundColor: colors.white }}>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40}
<View style={{ flex: 1, backgroundColor: colors.background }}>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : -30}
>
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
bounces={false}
showsVerticalScrollIndicator={false}
>
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled" bounces={false}>
<View style={{ paddingBottom: 20, marginTop: insets.top, flex: 1 }}>
<View style={{ paddingBottom: 20, marginTop: insets.top, flex: 1 }}>
{/* ═══ Back Button ═══ */}
<View style={styles.headerRow}>
<TouchableOpacity
onPress={() => {
if (stage > 0) {
@@ -518,12 +504,39 @@ useEffect(() => {
}
}}
style={styles.backBtn}
activeOpacity={0.7}
>
{/* Replaced AntDesign with Lucide ArrowLeft */}
<ArrowLeft size={25} color={colors.primary} />
<ArrowLeft size={22} color={colors.text} />
</TouchableOpacity>
<StepProgressBar currentStep={stage} steps={steps} />
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700' }}>
Registration
</AppText>
<View style={{ width: 36 }} />
</View>
{/* ═══ Progress Bar ═══ */}
<StepProgressBar currentStep={stage} steps={steps} />
{/* ═══ Stage Title ═══
<Animated.View
key={`title-${stage}`}
entering={FadeInUp.duration(300)}
style={styles.stageTitleContainer}
>
<AppText variant="h3" color={colors.text} style={styles.stageTitle}>
{stageTitles[stage]}
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
Step {stage + 1} of {steps.length}
</AppText>
</Animated.View> */}
{/* ═══ Stage Content ═══ */}
<Animated.View
key={`stage-${stage}`}
entering={FadeInUp.duration(350).delay(100)}
style={[styles.stageCard, shadows.medium]}
>
{stage === 0 && (
<StageZero
formData={formData}
@@ -549,7 +562,7 @@ useEffect(() => {
allMsmeOrgs={allMsmeOrgs}
allSocialCategories={allSocialCategories}
allMSME={allMSME}
dropdownStyle={dropdownStyle}
dropdownStyle={{}}
setSelectedOrg={setSelectedOrg}
selectedOrg={selectedOrg}
/>
@@ -594,8 +607,9 @@ useEffect(() => {
states={states}
cities={cities}
fetchCity={() => {
fetchCity(Number(formData.state))}}
dropdownStyle={dropdownStyle}
fetchCity(Number(formData.state));
}}
dropdownStyle={{}}
styles={styles}
/>
)}
@@ -609,10 +623,14 @@ useEffect(() => {
selectedOrg={selectedOrg}
/>
)}
</View>
</Animated.View>
</View>
{/* ═══ Next / Submit Button ═══ */}
<Animated.View entering={FadeInDown.duration(300).delay(200)}>
<PrimaryButton
title={stage === 4 ? (loading ? 'Loading...' : 'Submit') : 'Next'}
style={{ backgroundColor: colors.primary }}
onPress={() => {
if (validateStage(stage)) {
switch (stage) {
@@ -657,104 +675,105 @@ useEffect(() => {
}
}}
/>
</ScrollView>
</KeyboardAvoidingView>
</Animated.View>
</ScrollView>
</KeyboardAvoidingView>
</View>
);
};
export default Registration;
const styles = StyleSheet.create({
// ── Styles ────────────────────────────────────────────────────────────
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
scrollContainer: {
flexGrow: 1,
paddingHorizontal:14
paddingHorizontal: spacing.m,
},
headerRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: spacing.s,
},
backBtn: {
width: 36,
height: 36,
borderRadius: 10,
backgroundColor: colors.surface,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.border,
},
stageTitleContainer: {
marginBottom: spacing.m,
paddingHorizontal: spacing.xs,
},
stageTitle: {
fontWeight: '800',
marginBottom: 2,
},
stageCard: {
backgroundColor: colors.surface,
borderRadius: 16,
padding: spacing.m,
},
// These styles will be passed to child stages that still use `styles` prop
label: {
fontSize: 15,
fontWeight: '600',
marginVertical: 10,
color: colors.black,
fontSize: typography.size.s,
fontWeight: '700',
color: colors.primary,
marginBottom: spacing.s,
letterSpacing: 0.3,
textTransform: 'uppercase',
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 10,
},
backBtn: {
padding: 5,
width: 50,
marginBottom: 5,
},
sectionTitle: {
fontSize: 24,
fontSize: typography.size.l,
fontWeight: '700',
color: colors.black,
marginBottom: 20,
marginTop: 10,
color: colors.text,
marginBottom: spacing.m,
textAlign: 'center',
},
uploadGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
gap: spacing.s,
},
uploadCard: {
width: '48%',
backgroundColor: '#fdfdfd',
borderRadius: 20,
padding: 20,
backgroundColor: colors.surface,
borderRadius: 16,
padding: spacing.m,
alignItems: 'center',
marginBottom: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08,
shadowRadius: 6,
elevation: 4,
borderWidth: 1,
borderColor: '#ddd',
borderColor: colors.border,
},
iconCircle: {
width: 50,
height: 50,
borderRadius: 25,
width: 46,
height: 46,
borderRadius: 23,
backgroundColor: colors.primary,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 10,
marginBottom: spacing.s,
},
cardTitle: {
fontSize: 15,
fontSize: typography.size.s,
fontWeight: '600',
color: colors.black,
color: colors.text,
marginBottom: 4,
textAlign: 'center',
},
cardHint: {
fontSize: 12,
color: '#888',
fontSize: typography.size.xs,
color: colors.textSecondary,
textAlign: 'center',
},
dropdown: {
height: 55,
borderColor: '#e0e0e0',
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 14,
marginTop: 8,
marginBottom: 14,
backgroundColor: '#fff',
},
selectedTextStyle: {
fontSize: 14,
color: colors.black,
},
placeholderStyle: {
fontSize: 14,
color: colors.black,
},
itemTextStyle: {
fontSize: 14,
color: colors.black,
},
});
+118 -80
View File
@@ -3,24 +3,21 @@ import {
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
Platform,
} from 'react-native';
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import colors from '../../Constant/Color';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
import MyInput from '../../Component/MyInput';
import MyInput from '../../components/MyInput';
import { showToast } from '../../Constant/ShowDailog';
import { STAGETHREE } from '../../Services/ApiCall/ApiUrls';
import { useContractorStageThreeSubmit } from '../../Services/ApiCall/Hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import { Trash2, Building2 } from 'lucide-react-native';
type Company = {
director: string;
@@ -32,13 +29,16 @@ type Company = {
type StageThreeRouteParams = {
orgType: string;
otherId: string | number
otherId: string | number;
};
const StageThreeScreen = () => {
const route = useRoute<RouteProp<{ params: StageThreeRouteParams }, 'params'>>();
const navigation = useNavigation<any>();
const insets = useSafeAreaInsets()
const { colors, spacing, typography, shadows } = useTheme();
const insets = useSafeAreaInsets();
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
const [company, setCompany] = useState<Company[]>([
{
director: '',
@@ -52,7 +52,7 @@ const StageThreeScreen = () => {
const [focusedField, setFocusedField] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const { mutateAsync: stageThreeSubmit } = useContractorStageThreeSubmit();
const handleChange = (index: number, field: keyof Company, value: string) => {
const updated = [...company];
updated[index][field] = value;
@@ -72,7 +72,6 @@ const StageThreeScreen = () => {
]);
};
const handleSubmit = async () => {
setLoading(true);
try {
@@ -119,140 +118,179 @@ const StageThreeScreen = () => {
);
};
const orgLabel =
route.params?.orgType == '4'
? 'Public'
: route.params?.orgType == '5'
? 'Private'
: route.params?.orgType == '6'
? 'LLP'
: route.params?.orgType;
return (
<View style={{ flex: 1 ,paddingHorizontal:10 }}>
<Text style={[styles.heading,{marginTop:insets.top}]}>Stage Three For {route.params?.orgType == '4' ? 'Public' : route.params?.orgType == '5' ? 'Private' : route.params?.orgType == '6' ? 'Limited Liability Partnership (LLP)' : route.params?.orgType} Organization</Text>
<View style={{ flex: 1, backgroundColor: colors.background }}>
<View style={[styles.header, { paddingTop: insets.top + spacing.s }]}>
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700' }}>
{orgLabel} Organization
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
Add directors / shareholders for your organization
</AppText>
</View>
<SafeAreaView style={{ flex: 1 }}>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40}
>
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled">
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{company.map((partner, index) => (
<View style={{ marginBottom: 20 }} key={index}>
<Text style={styles.partnerTitle}>Company {index + 1}</Text>
<Animated.View
key={index}
entering={FadeInUp.delay(index * 80).springify()}
style={[styles.card, shadows.medium]}
>
<View style={styles.cardHeader}>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
Director {index + 1}
</AppText>
{company.length > 1 && (
<TouchableOpacity onPress={() => handleRemove(index)} activeOpacity={0.7}>
<Trash2 size={18} color={colors.error} />
</TouchableOpacity>
)}
</View>
<MyInput
icon="user"
placeholder="Name of Director *"
label="Director Name"
placeholder="Enter Name of Director"
value={partner.director}
onChangeText={(text:any) => handleChange(index, 'director', text)}
onChangeText={(text: any) => handleChange(index, 'director', text)}
onFocus={() => setFocusedField(`director-${index}`)}
onBlur={() => setFocusedField(null)}
focused={focusedField === `director-${index}`}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="id-card"
placeholder="DIN Number in Firm *"
label="DIN Number"
placeholder="Enter DIN Number"
value={partner.DINNumber}
onChangeText={(text:any) => handleChange(index, 'DINNumber', text)}
onChangeText={(text: any) => handleChange(index, 'DINNumber', text)}
onFocus={() => setFocusedField(`DINNumber-${index}`)}
onBlur={() => setFocusedField(null)}
focused={focusedField === `DINNumber-${index}`}
keyboardType="numeric"
maxlength={8}
maxLength={8}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="id-card"
placeholder="PAN of Director *"
label="PAN of Director"
placeholder="Enter PAN Number"
value={partner.panNumber}
onChangeText={(text:any) => handleChange(index, 'panNumber', text)}
onChangeText={(text: any) => handleChange(index, 'panNumber', text)}
onFocus={() => setFocusedField(`panNumber-${index}`)}
onBlur={() => setFocusedField(null)}
focused={focusedField === `panNumber-${index}`}
maxlength={12}
maxLength={12}
autoCapitalize="characters"
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="credit-card"
placeholder="Aadhar Number *"
label="Aadhaar Number"
placeholder="Enter 12-digit Aadhaar"
value={partner.aadharNumber}
onChangeText={(text:any) => handleChange(index, 'aadharNumber', text)}
onChangeText={(text: any) => handleChange(index, 'aadharNumber', text)}
onFocus={() => setFocusedField(`aadharNumber-${index}`)}
onBlur={() => setFocusedField(null)}
focused={focusedField === `aadharNumber-${index}`}
keyboardType="numeric"
maxlength={12}
maxLength={12}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="phone"
placeholder="Mobile Number *"
label="Mobile Number"
placeholder="Enter Mobile Number"
value={partner.mobileNumber}
onChangeText={(text:any) => handleChange(index, 'mobileNumber', text)}
onChangeText={(text: any) => handleChange(index, 'mobileNumber', text)}
onFocus={() => setFocusedField(`mobileNumber-${index}`)}
onBlur={() => setFocusedField(null)}
focused={focusedField === `mobileNumber-${index}`}
keyboardType="phone-pad"
maxlength={10}
maxLength={10}
/>
{company.length > 1 && (
<TouchableOpacity style={styles.deleteButton} onPress={() => handleRemove(index)}>
<Text style={styles.deleteText}>Delete Company</Text>
</TouchableOpacity>
)}
</View>
</Animated.View>
))}
<TouchableOpacity style={styles.addButton} onPress={handleAddMore}>
<Text style={styles.addText}>+ Add Company</Text>
<TouchableOpacity style={styles.addButton} onPress={handleAddMore} activeOpacity={0.8}>
<Building2 size={16} color={colors.buttonText} />
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700', marginLeft: 8 }}>
Add Director
</AppText>
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} />
<View style={styles.footer}>
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} />
</View>
</View>
);
};
export default StageThreeScreen;
const styles = StyleSheet.create({
heading: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
marginTop: 10,
textAlign: 'center',
},
partnerTitle: {
fontWeight: '600',
fontSize: 16,
marginBottom: 10,
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
header: {
paddingHorizontal: spacing.m,
paddingBottom: spacing.m,
borderBottomWidth: 1,
borderBottomColor: colors.border,
backgroundColor: colors.surface,
},
scrollContainer: {
flexGrow: 1,
padding: spacing.m,
paddingBottom: 30,
},
card: {
backgroundColor: colors.surface,
borderRadius: 14,
padding: 14,
marginBottom: spacing.m,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.s,
paddingBottom: spacing.xs,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
},
addButton: {
backgroundColor: colors.primary,
padding: 10,
marginTop: 10,
borderRadius: 10,
flexDirection: 'row',
alignItems: 'center',
width: 140,
justifyContent: 'center',
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 12,
alignSelf: 'flex-end',
},
addText: {
color: 'white',
fontWeight: '600',
},
deleteButton: {
backgroundColor: '#FF5C5C',
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 140,
alignSelf: 'flex-end',
},
deleteText: {
color: 'white',
fontWeight: '600',
footer: {
padding: spacing.m,
borderTopWidth: 1,
borderTopColor: colors.border,
backgroundColor: colors.surface,
},
});
@@ -1,5 +1,5 @@
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { View, TouchableOpacity } from 'react-native';
import {
IdCard,
FileText,
@@ -7,7 +7,12 @@ import {
ShieldCheck,
Building2,
ScrollText,
} from 'lucide-react-native'; // lucide icons
CheckCircle,
Upload,
} from 'lucide-react-native';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface DocumentItem {
key: string;
@@ -30,13 +35,15 @@ const StageFour: React.FC<UploadDocumentsSectionProps> = ({
formData,
selectedOrg,
}) => {
const { colors, spacing, shadows } = useTheme();
const documentItems: DocumentItem[] = [
{ key: 'panCard', label: 'PAN Card', icon: 'pan' },
{ key: 'aadharCard', label: 'Aadhar Card', icon: 'aadhar' },
{ key: 'gstnCertificate', label: 'GSTN Certificate', icon: 'gst' },
{ key: 'constractorClass', label: 'Contractor Class File', icon: 'contractor' },
{ key: 'constractorClass', label: 'Contractor Class', icon: 'contractor' },
{ key: 'msmefile', label: 'MSME Organization', icon: 'msme' },
{ key: 'registrationCertificate', label: 'Registration Certificate', icon: 'registration' },
{ key: 'registrationCertificate', label: 'Registration Cert.', icon: 'registration' },
];
const shouldRenderItem = (itemKey: string): boolean => {
@@ -52,54 +59,84 @@ const StageFour: React.FC<UploadDocumentsSectionProps> = ({
return false;
}
return true; // pan + aadhar + rest (unless filtered above)
return true;
};
const renderIcon = (iconType: DocumentItem['icon']) => {
const commonProps = { size: 26, color: '#fff', strokeWidth: 2 };
const commonProps = { size: 24, color: colors.buttonText, strokeWidth: 2 };
switch (iconType) {
case 'pan':
return <IdCard {...commonProps} />; // PAN → ID card feel [web:19]
return <IdCard {...commonProps} />;
case 'aadhar':
return <IdCard {...commonProps} />; // Same icon, different label
return <IdCard {...commonProps} />;
case 'gst':
return <FileBadge {...commonProps} />; // File with badge [web:20]
return <FileBadge {...commonProps} />;
case 'contractor':
return <ShieldCheck {...commonProps} />; // Safety / verification feel
return <ShieldCheck {...commonProps} />;
case 'msme':
return <Building2 {...commonProps} />; // Organization / company
return <Building2 {...commonProps} />;
case 'registration':
return <ScrollText {...commonProps} />; // Certificate / document scroll
return <ScrollText {...commonProps} />;
default:
return <FileText {...commonProps} />;
}
};
let animIndex = 0;
return (
<>
<Text style={styles.sectionTitle}>Upload Required Documents</Text>
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700', textAlign: 'center', marginBottom: spacing.m }}>
Upload Documents
</AppText>
<View style={styles.uploadGrid}>
<View style={{
flexDirection: 'row',
flexWrap: 'wrap',
}}>
{documentItems.map(item => {
if (!shouldRenderItem(item.key)) return null;
const isUploaded = !!documents[item.key];
const currentIndex = animIndex++;
return (
<TouchableOpacity
<Animated.View
key={item.key}
style={styles.uploadCard}
onPress={() => handleDocumentPick(item.key)}
entering={FadeInUp.delay(80 * currentIndex).springify()}
style={{ width: '50%', padding: spacing.xs }}
>
<View style={styles.iconCircle}>{renderIcon(item.icon)}</View>
<TouchableOpacity
style={[
styles.uploadCard,
{ width: '100%' },
{
borderColor: isUploaded ? colors.primary : colors.border,
backgroundColor: isUploaded ? `${colors.primary}08` : colors.surface,
},
]}
onPress={() => handleDocumentPick(item.key)}
activeOpacity={0.7}
>
<View style={[styles.iconCircle, isUploaded && { backgroundColor: colors.primary }]}>
{isUploaded ? (
<CheckCircle size={24} color={colors.buttonText} strokeWidth={2.5} />
) : (
renderIcon(item.icon)
)}
</View>
<Text style={styles.cardTitle}>{item.label}</Text>
<AppText variant="bodySmall" color={colors.text} style={{ fontWeight: '600', textAlign: 'center', marginBottom: 4 }}>
{item.label}
</AppText>
<Text style={styles.cardHint}>
{documents[item.key]
? `${documents[item.key]?.name} ✅`
: 'Tap to upload PDF'}
</Text>
</TouchableOpacity>
<AppText variant="caption" color={isUploaded ? colors.primary : colors.textSecondary} style={{ textAlign: 'center', fontSize: 10 }}>
{isUploaded
? `${documents[item.key]?.name?.slice(0, 18)}...`
: 'Tap to upload PDF'}
</AppText>
</TouchableOpacity>
</Animated.View>
);
})}
</View>
@@ -1,11 +1,11 @@
import React, { useState } from 'react';
import { Text, View, StyleSheet } from 'react-native';
import DatePicker from 'react-native-date-picker'
import MyInput from '../../../Component/MyInput';
import { View } from 'react-native';
import DatePicker from 'react-native-date-picker';
import MyInput from '../../../components/MyInput';
import MyDropdown from '../../../Component/Dropdown/MyDropdown';
import colors from '../../../Constant/Color';
import { DropdownOption } from '../types/types';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props {
formData: {
@@ -14,7 +14,7 @@ interface Props {
orgType: string;
registrationDetail: string | number;
socialCategory: string;
expiryDate: string
expiryDate: string;
};
handleChange: (key: string, value: string) => void;
focusedField: string | null;
@@ -26,7 +26,7 @@ interface Props {
allMSME: DropdownOption[];
dropdownStyle: object;
setSelectedOrg: (val: number) => void;
selectedOrg: string | number
selectedOrg: string | number;
}
const StageOne: React.FC<Props> = ({
@@ -40,23 +40,24 @@ const StageOne: React.FC<Props> = ({
allSocialCategories,
dropdownStyle,
setSelectedOrg,
selectedOrg
selectedOrg,
}) => {
console.log("masterdata",allOrganizations,allMsmeOrgs,allSocialCategories,contractorClassItems,selectedOrg !== 8)
const [selectedAllMSME, setSelectedAllMSME] = useState()
const [open, setOpen] = useState(false)
const [date, setDate] = useState(new Date())
const { colors, spacing } = useTheme();
const [selectedAllMSME, setSelectedAllMSME] = useState();
const [open, setOpen] = useState(false);
const [date, setDate] = useState(new Date());
const handleDateConfirm = (selectedDate: Date) => {
setOpen(false);
setDate(selectedDate);
handleChange('expiryDate', selectedDate.toISOString().split('T')[0]); // Format as YYYY-MM-DD
handleChange('expiryDate', selectedDate.toISOString().split('T')[0]);
};
return (
<View>
<Text style={styles.label}>Registration & Type Details</Text>
<AppText variant="caption" color={colors.primary} style={{ fontWeight: '700', letterSpacing: 0.3, textTransform: 'uppercase', marginBottom: spacing.s }}>
Registration & Type Details
</AppText>
<MyDropdown
label="Type of Organization"
@@ -69,24 +70,26 @@ const StageOne: React.FC<Props> = ({
setSelectedOrg(item.value);
handleChange('orgType', item.value);
}}
style={dropdownStyle}
onFocus={() => setFocusedField('orgType')}
onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'orgType'}
/>
{
selectedOrg !== 8 &&
{selectedOrg !== 8 && (
<MyInput
icon="file"
placeholder="Registration Number *"
label="Registration Number"
placeholder="Enter Registration Number"
value={formData.registrationNumber}
onChangeText={text => handleChange('registrationNumber', text)}
onFocus={() => setFocusedField('registrationNumber')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'registrationNumber'}
autoCapitalize='characters' />
}
{selectedOrg &&
autoCapitalize="characters"
containerStyle={{ marginBottom: spacing.s }}
/>
)}
{selectedOrg && (
<MyDropdown
label="Contractor Class"
data={contractorClassItems}
@@ -94,7 +97,6 @@ const StageOne: React.FC<Props> = ({
valueField="value"
placeholder="Select Class"
value={selectedOrg == 8 ? contractorClassItems[0]?.value : formData.contractorClass}
style={dropdownStyle}
disable={selectedOrg == 8 ? true : false}
onChange={item => {
handleChange('contractorClass', item.value);
@@ -102,9 +104,10 @@ const StageOne: React.FC<Props> = ({
onFocus={() => setFocusedField('contractorClass')}
onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'contractorClass'}
/>}
/>
)}
{selectedOrg !== 8 &&
{selectedOrg !== 8 && (
<MyDropdown
label="Registration Detail (MSME/NSIC etc.)"
data={allMsmeOrgs}
@@ -113,32 +116,15 @@ const StageOne: React.FC<Props> = ({
placeholder="Select Registration Type"
value={formData.registrationDetail}
onChange={item => {
setSelectedAllMSME(item.value)
console.log('first', item)
handleChange('registrationDetail', item.value)
setSelectedAllMSME(item.value);
handleChange('registrationDetail', item.value);
}}
style={dropdownStyle}
onFocus={() => setFocusedField('registrationDetail')}
onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'registrationDetail'}
/>}
{/* <MyDropdown
label="MSME Details"
data={allMSME}
labelField="label"
valueField="value"
placeholder="Select MSME Type"
value={''}
onChange={item => {
setSelectedAllMSME(item.value)
handleChange('allMsme', item.value)
}}
style={dropdownStyle}
/>
)}
onFocus={() => setFocusedField('allMsme')}
onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'allMsme'}
/> */}
<MyDropdown
label="Social Category"
data={allSocialCategories}
@@ -147,25 +133,21 @@ const StageOne: React.FC<Props> = ({
placeholder="Select Social Category"
value={formData.socialCategory}
onChange={item => handleChange('socialCategory', item.value)}
style={dropdownStyle}
onFocus={() => setFocusedField('socialCategory')}
onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'socialCategory'}
/>
{
selectedAllMSME !== 3 && selectedOrg !== 8 &&
<>
{selectedAllMSME !== 3 && selectedOrg !== 8 && (
<>
<MyInput
icon="calendar-alt"
placeholder="Expiry Date *"
label="Expiry Date"
placeholder="Select Expiry Date"
value={formData.expiryDate}
onChangeText={text => handleChange('expiryDate', text)}
onFocus={() => setFocusedField('expiryDate')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'expiryDate'}
rightIcon='wallet'
editable={true}
rightIconPress={() => setOpen(true)}
/>
@@ -175,22 +157,12 @@ const StageOne: React.FC<Props> = ({
date={date}
onConfirm={handleDateConfirm}
onCancel={() => setOpen(false)}
mode="date" // Ensure you're using date mode
mode="date"
/>
</>
}
)}
</View>
);
};
const styles = StyleSheet.create({
label: {
fontSize: 16,
fontWeight: '600',
color: colors.black,
marginBottom: 10,
},
});
export default StageOne;
@@ -1,10 +1,10 @@
import React from 'react';
import { Text } from 'react-native';
import { View } from 'react-native';
import { DropdownOption } from '../types/types';
import MyInput from '../../../Component/MyInput';
import MyInput from '../../../components/MyInput';
import MyDropdown from '../../../Component/Dropdown/MyDropdown';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface AddressDetailsSectionProps {
formData: {
@@ -21,7 +21,7 @@ interface AddressDetailsSectionProps {
cities: DropdownOption[];
fetchCity: (stateValue: number) => void;
dropdownStyle?: object;
styles?: any; // Optional styles prop for custom styling
styles?: any;
}
const StageThree: React.FC<AddressDetailsSectionProps> = ({
@@ -32,54 +32,51 @@ const StageThree: React.FC<AddressDetailsSectionProps> = ({
states,
cities,
fetchCity,
styles
styles,
}) => {
const { colors, spacing } = useTheme();
const dropdownStyle = {
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
backgroundColor: '#fff',
marginBottom: 10,
};
return (
<>
<Text style={styles.label}>Address Details</Text>
<AppText variant="caption" color={colors.primary} style={styles.label}>
Address Details
</AppText>
<MyInput
icon="home"
placeholder="Enter Address *"
label="Address"
placeholder="Enter Full Address"
value={formData.address}
onChangeText={text => handleChange('address', text)}
onFocus={() => setFocusedField('address')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'address'}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
maxlength={10}
icon="phone"
placeholder="Contact Number *"
label="Contact Number"
maxLength={10}
placeholder="Enter Contact Number"
value={formData.agentContact2}
onChangeText={text => handleChange('agentContact2', text)}
onFocus={() => setFocusedField('agentContact2')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'agentContact2'}
keyboardType="numeric"
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="lock"
placeholder="Pin Code *"
label="Pin Code"
placeholder="Enter 6-digit Pin Code"
keyboardType="numeric"
maxlength={6}
maxLength={6}
value={formData.pinCode}
onChangeText={text => handleChange('pinCode', text)}
onFocus={() => setFocusedField('pinCode')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'pinCode'}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyDropdown
@@ -94,7 +91,6 @@ const StageThree: React.FC<AddressDetailsSectionProps> = ({
handleChange('city', '');
fetchCity(item.value);
}}
style={dropdownStyle}
onFocus={() => setFocusedField('state')}
onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'state'}
@@ -108,7 +104,6 @@ const StageThree: React.FC<AddressDetailsSectionProps> = ({
placeholder="Select City"
value={formData.city}
onChange={item => handleChange('city', item.value)}
style={dropdownStyle}
onFocus={() => setFocusedField('city')}
onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'city'}
@@ -1,13 +1,13 @@
import React from 'react';
import {
Text,
TouchableOpacity,
ActivityIndicator,
View,
Alert,
} from 'react-native';
import MyInput from '../../../Component/MyInput';
// Adjust the path to your custom input
import MyInput from '../../../components/MyInput';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props {
formData: {
@@ -21,12 +21,10 @@ interface Props {
handleChange: (key: string, value: string) => void;
focusedField: string | null;
setFocusedField: (field: string | null | any) => void;
// PAN
panVerified: boolean;
setPanVerified: (value: boolean) => void;
verifyPanHandler: () => void;
panVerifying: boolean;
// Aadhaar
aadhaarVerified: boolean;
setAadhaarVerified: (value: boolean) => void;
sendAdharOTPHandler: (aadhaarNumber: string) => void;
@@ -34,13 +32,12 @@ interface Props {
setadharotpsend: (value: boolean) => void;
verifyAdharOtpHandler: () => void;
aadhaarVerifying: boolean;
// GST
verifyGSTHandler: () => void;
gstverifying: boolean;
color: any;
styles: any;
formatDateInput: (text: string) => string;
selectedOrg:string|number
selectedOrg: string | number;
}
const StageTwo: React.FC<Props> = ({
@@ -64,61 +61,79 @@ const StageTwo: React.FC<Props> = ({
color,
styles,
formatDateInput,
selectedOrg
selectedOrg,
}) => {
const { colors, spacing, shadows } = useTheme();
const verifyButtonStyle = {
backgroundColor: colors.primary,
paddingVertical: 10,
paddingHorizontal: 16,
marginTop: spacing.s,
borderRadius: 10,
alignItems: 'center' as const,
alignSelf: 'flex-end' as const,
minWidth: 120,
};
return (
<>
{/* ---------- PAN Details ---------- */}
<Text style={styles.label}>PAN Details</Text>
{/* ── PAN Details ── */}
<AppText variant="caption" color={colors.primary} style={styles.label}>
PAN Details
</AppText>
<MyInput
icon="file"
placeholder={selectedOrg == 8 ? "Pan Number *" : "Organization PAN Number *"}
label={selectedOrg == 8 ? "PAN Number" : "Organization PAN Number"}
placeholder={selectedOrg == 8 ? "Enter PAN Number" : "Enter Organization PAN"}
value={formData.orgPanNumber}
onChangeText={(text) => {
setPanVerified(false);
handleChange('orgPanNumber', text);
}}
isVarify={panVerified}
isVerify={panVerified}
onFocus={() => setFocusedField('pan')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'pan'}
editable={!panVerified}
autoCapitalize="characters"
maxlength={10}
maxLength={10}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="calendar"
placeholder={selectedOrg == 8 ? "Date of Birth *" :"Date of Incorporation *"}
label={selectedOrg == 8 ? "Date of Birth" : "Date of Incorporation"}
placeholder="DD/MM/YYYY"
value={formData.dateOfIncorp}
onChangeText={(text) => {
const formatted = formatDateInput(text);
setPanVerified(false);
handleChange('dateOfIncorp', formatted);
}}
isVarify={panVerified}
isVerify={panVerified}
onFocus={() => setFocusedField('dob')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'dob'}
editable={!panVerified}
keyboardType='numeric'
keyboardType="numeric"
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="file-signature"
placeholder="PAN Name *"
label="PAN Name"
placeholder="Enter Name as on PAN"
value={formData.orgPanName}
onChangeText={(text) => {
setPanVerified(false);
handleChange('orgPanName', text);
}}
isVarify={panVerified}
isVerify={panVerified}
onFocus={() => setFocusedField('panName')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'panName'}
editable={!panVerified}
autoCapitalize="characters"
containerStyle={{ marginBottom: spacing.s }}
/>
{formData.orgPanNumber &&
@@ -126,139 +141,187 @@ const StageTwo: React.FC<Props> = ({
formData.orgPanName &&
!panVerified && (
<TouchableOpacity
style={{
backgroundColor: color.primary,
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 120,
alignSelf: 'flex-end',
}}
style={verifyButtonStyle}
disabled={panVerifying}
onPress={verifyPanHandler}
activeOpacity={0.8}
>
<Text style={{ color: 'white', fontWeight: '600' }}>
{panVerifying ? 'Verifying' : 'Verify PAN'}
</Text>
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700' }}>
{panVerifying ? 'Verifying...' : 'Verify PAN'}
</AppText>
</TouchableOpacity>
)}
{/* ---------- Aadhaar Details ---------- */}
<Text style={styles.label}>Aadhaar Details</Text>
<MyInput
icon="address-card"
placeholder="Aadhaar Number *"
keyboardType="numeric"
maxlength={12}
value={formData.aadhaarNumber}
onChangeText={(text) => {
handleChange('aadhaarNumber', text);
setAadhaarVerified(false);
setadharotpsend(false);
handleChange('aadhaarOtp', '');
}}
isVarify={aadhaarVerified}
onFocus={() => setFocusedField('adhar')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'adhar'}
editable={!aadhaarVerified}
/>
{formData.aadhaarNumber.length === 12 && !adharotpsend && (
{panVerified && (
<TouchableOpacity
style={{
backgroundColor: color.primary,
padding: 10,
marginTop: 10,
borderWidth: 1.5,
borderColor: '#E67E22',
paddingVertical: 10,
paddingHorizontal: 16,
marginTop: spacing.s,
borderRadius: 10,
alignItems: 'center',
width: 120,
alignSelf: 'flex-end',
alignItems: 'center' as const,
alignSelf: 'flex-end' as const,
minWidth: 120,
backgroundColor: 'transparent',
}}
onPress={() => sendAdharOTPHandler(formData.aadhaarNumber)}
disabled={aadhaarVerifying}
onPress={() => {
Alert.alert(
'Change PAN',
'Are you sure you want to change your PAN details? You will need to re-verify.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Change',
style: 'destructive',
onPress: () => {
setPanVerified(false);
handleChange('orgPanNumber', '');
handleChange('dateOfIncorp', '');
handleChange('orgPanName', '');
},
},
],
);
}}
activeOpacity={0.8}
>
{aadhaarVerifying ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={{ color: 'white', fontWeight: '600' }}>Send OTP</Text>
)}
<AppText variant="bodySmall" color={'#E67E22'} style={{ fontWeight: '700' }}>
Change PAN
</AppText>
</TouchableOpacity>
)}
{adharotpsend && !aadhaarVerified && (
<>
<MyInput
icon="lock"
placeholder="Enter OTP *"
keyboardType="numeric"
maxlength={6}
value={formData.aadhaarOtp}
onChangeText={(text) => handleChange('aadhaarOtp', text)}
onFocus={() => setFocusedField('adharOtp')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'adharOtp'}
/>
{/* ── Aadhaar Details ── */}
<View style={{ marginTop: spacing.m, borderTopWidth: 1, borderTopColor: colors.border, paddingTop: spacing.m }}>
<AppText variant="caption" color={colors.primary} style={styles.label}>
Aadhaar Details
</AppText>
<MyInput
label="Aadhaar Number"
placeholder="Enter 12-digit Aadhaar Number"
keyboardType="numeric"
maxLength={12}
value={formData.aadhaarNumber}
onChangeText={(text) => {
handleChange('aadhaarNumber', text);
setAadhaarVerified(false);
setadharotpsend(false);
handleChange('aadhaarOtp', '');
}}
isVerify={aadhaarVerified}
onFocus={() => setFocusedField('adhar')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'adhar'}
editable={!aadhaarVerified}
containerStyle={{ marginBottom: spacing.s }}
/>
{formData.aadhaarNumber.length === 12 && !adharotpsend && (
<TouchableOpacity
style={verifyButtonStyle}
onPress={() => sendAdharOTPHandler(formData.aadhaarNumber)}
disabled={aadhaarVerifying}
activeOpacity={0.8}
>
{aadhaarVerifying ? (
<ActivityIndicator color={colors.buttonText} size="small" />
) : (
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700' }}>
Send OTP
</AppText>
)}
</TouchableOpacity>
)}
{adharotpsend && !aadhaarVerified && (
<>
<MyInput
label="OTP"
placeholder="Enter 6-digit OTP"
keyboardType="numeric"
maxLength={6}
value={formData.aadhaarOtp}
onChangeText={(text) => handleChange('aadhaarOtp', text)}
onFocus={() => setFocusedField('adharOtp')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'adharOtp'}
containerStyle={{ marginBottom: spacing.s }}
/>
<TouchableOpacity
style={verifyButtonStyle}
onPress={verifyAdharOtpHandler}
disabled={aadhaarVerifying}
activeOpacity={0.8}
>
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700' }}>
{aadhaarVerifying ? 'Verifying...' : 'Verify OTP'}
</AppText>
</TouchableOpacity>
</>
)}
{aadhaarVerified && (
<TouchableOpacity
style={{
backgroundColor: color.primary,
padding: 10,
marginTop: 10,
borderWidth: 1.5,
borderColor: '#E67E22',
paddingVertical: 10,
paddingHorizontal: 16,
marginTop: spacing.s,
borderRadius: 10,
alignItems: 'center',
width: 120,
alignSelf: 'flex-end',
alignItems: 'center' as const,
alignSelf: 'flex-end' as const,
minWidth: 120,
backgroundColor: 'transparent',
}}
onPress={verifyAdharOtpHandler}
disabled={aadhaarVerifying}
onPress={() => {
Alert.alert(
'Change Aadhaar',
'Are you sure you want to change your Aadhaar details? You will need to re-verify.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Change',
style: 'destructive',
onPress: () => {
setAadhaarVerified(false);
setadharotpsend(false);
handleChange('aadhaarNumber', '');
handleChange('aadhaarOtp', '');
},
},
],
);
}}
activeOpacity={0.8}
>
<Text style={{ color: 'white', fontWeight: '600' }}>
{aadhaarVerifying ? 'Verifying...' : 'Verify'}
</Text>
<AppText variant="bodySmall" color={'#E67E22'} style={{ fontWeight: '700' }}>
Change Aadhaar
</AppText>
</TouchableOpacity>
</>
)}
</View>
{/* ── GST Section ── */}
{selectedOrg !== 11 && (
<View style={{ marginTop: spacing.m, borderTopWidth: 1, borderTopColor: colors.border, paddingTop: spacing.m }}>
<MyInput
label="GSTN Number"
placeholder="Enter GSTN Number"
value={formData.gstnNo}
onChangeText={(text) => handleChange('gstnNo', text)}
onFocus={() => setFocusedField('gstNumber')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'gstNumber'}
autoCapitalize="characters"
/>
</View>
)}
{/* ---------- GST Section ---------- */}
{selectedOrg !== 11 &&
<>
<MyInput
icon="file-invoice"
placeholder="GSTN No"
value={formData.gstnNo}
onChangeText={(text) => handleChange('gstnNo', text)}
onFocus={() => setFocusedField('gstNumber')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'gstNumber'}
autoCapitalize="characters"
/>
{/* {formData?.gstnNo?.length === 15 && (
<TouchableOpacity
style={{
backgroundColor: color.primary,
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 120,
alignSelf: 'flex-end',
}}
onPress={verifyGSTHandler}
>
<Text style={{ color: 'white', fontWeight: '600' }}>
{gstverifying ? 'Verifying...' : 'Verify GST'}
</Text>
</TouchableOpacity>
)} */}
</>}
</>
);
};
export default StageTwo;
@@ -1,12 +1,12 @@
import React, { useEffect } from 'react';
import { Text, View, StyleSheet } from 'react-native';
import MyInput from '../../../Component/MyInput';
import React from 'react';
import { View } from 'react-native';
import MyInput from '../../../components/MyInput';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props {
formData: {
agentContact2: string;
organizationName: string;
officialWebsite: string;
@@ -14,7 +14,7 @@ interface Props {
email: string;
firstName: string;
middleName: string;
lastName: string
lastName: string;
};
handleChange: (key: string, value: string) => void;
focusedField: string | null;
@@ -29,52 +29,55 @@ const StageZero: React.FC<Props> = ({
setFocusedField,
styles,
}) => {
const { colors, spacing } = useTheme();
return (
<>
<Text style={styles.label}>Personal & Contact Details</Text>
{/* <View style={styles.row}> */}
<AppText variant="caption" color={colors.primary} style={styles.label}>
Personal & Contact Details
</AppText>
<MyInput
icon="user"
placeholder="First Name *"
label="First Name"
placeholder="First Name"
value={formData.firstName}
onChangeText={(text: string) => handleChange('firstName', text)}
onFocus={() => setFocusedField('firstname')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'firstname'}
editable={false}
containerStyle={{ marginBottom: spacing.s }}
/>
<View style={{ width: 10 }} />
{
formData.middleName ?
<MyInput
icon="user"
placeholder="Middle Name"
value={formData.middleName}
onChangeText={(text: string) => handleChange('middleName', text)}
onFocus={() => setFocusedField('middleName')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'middleName'}
editable={false}
/>
: null}
{formData.middleName ? (
<MyInput
label="Middle Name"
placeholder="Middle Name"
value={formData.middleName}
onChangeText={(text: string) => handleChange('middleName', text)}
onFocus={() => setFocusedField('middleName')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'middleName'}
editable={false}
containerStyle={{ marginBottom: spacing.s }}
/>
) : null}
<MyInput
icon="user"
placeholder="Last Name *"
label="Last Name"
placeholder="Last Name"
value={formData.lastName}
onChangeText={(text: string) => handleChange('lastName', text)}
onFocus={() => setFocusedField('lastName')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'lastName'}
editable={false}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="envelope"
placeholder="Email *"
label="Email"
placeholder="Email Address"
keyboardType="email-address"
value={formData.email}
onChangeText={(text: string) => handleChange('email', text)}
@@ -82,11 +85,12 @@ const StageZero: React.FC<Props> = ({
onBlur={() => setFocusedField(null)}
focused={focusedField === 'email'}
editable={false}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="phone"
placeholder="Mobile Number *"
label="Mobile Number"
placeholder="Mobile Number"
keyboardType="phone-pad"
value={formData.mobileNumber}
onChangeText={(text: string) => handleChange('mobileNumber', text)}
@@ -94,22 +98,24 @@ const StageZero: React.FC<Props> = ({
onBlur={() => setFocusedField(null)}
focused={focusedField === 'mobileNumber'}
editable={false}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="building"
placeholder="Organization Name *"
label="Organization Name"
placeholder="Enter Organization Name"
value={formData.organizationName}
onChangeText={(text: string) => handleChange('organizationName', text)}
onFocus={() => setFocusedField('organizationName')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'organizationName'}
autoCapitalize="none"
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="globe"
placeholder="Official Website (optional)"
label="Official Website"
placeholder="Website (optional)"
value={formData.officialWebsite}
onChangeText={(text: string) => handleChange('officialWebsite', text)}
onFocus={() => setFocusedField('officialWebsite')}
+273 -294
View File
@@ -1,9 +1,7 @@
import { useNavigation } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
@@ -13,16 +11,18 @@ import {
ScrollView,
ToastAndroid,
} from 'react-native';
import colors from '../../Constant/Color';
import { User, Phone, Mail, Lock, Eye, EyeOff, ArrowLeft, CheckSquare, Square } from 'lucide-react-native';
import { User, Phone, Mail, Lock, CheckSquare, Square, Shield, UserPlus, Contact, Eye, EyeOff } from 'lucide-react-native';
import { useGetDesignation, useRegisterContractor } from '../../Services/ApiCall/Hooks';
import MyDropdown from '../../Component/Dropdown/MyDropdown';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
import PrimaryButton from '../../components/PrimaryButton';
import MyInput from '../../components/MyInput';
import InsideHeader from '../../Component/Header/InsideHeader';
import TermsAndConditionsModal from '../../Component/TermsAndConditionsModal/TermsAndConditionsModal';
import AppText from '../../components/AppText';
import { Formik } from 'formik';
import * as Yup from 'yup';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { InsideHeader } from '../../Component/Index';
import Animated, { FadeInUp, FadeInDown } from 'react-native-reanimated';
import { useTheme } from '../../Hooks/useTheme';
export type ContractorFormData = {
firstName: string;
@@ -40,7 +40,7 @@ export type ContractorFormData = {
type DesignationOption = {
label: string;
value: string; // string id from API
value: string;
};
const SignupSchema = Yup.object().shape({
@@ -48,7 +48,7 @@ const SignupSchema = Yup.object().shape({
firstName: Yup.string().required('First name is required.'),
middleName: Yup.string().optional(),
lastName: Yup.string().required('Last name is required.'),
designation: Yup.string().required('Please select a designation.'), // string
designation: Yup.string().required('Please select a designation.'),
mobileNumber: Yup.string()
.matches(/^[6-9]\d{9}$/, 'Invalid mobile number.')
.required('Mobile number is required.'),
@@ -65,6 +65,7 @@ const SignupSchema = Yup.object().shape({
});
const Signup = () => {
const { colors, spacing, typography, shadows } = useTheme();
const navigation = useNavigation<any>();
const [designation, setDesignation] = useState<DesignationOption[]>([]);
const { data, isLoading: isDesignationLoading } = useGetDesignation();
@@ -74,6 +75,7 @@ const Signup = () => {
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
useEffect(() => {
const list = (data as any)?.data;
@@ -81,89 +83,15 @@ const Signup = () => {
const formattedDesignation: DesignationOption[] = list.map((item: any) => ({
label: item.designationName,
value: String(item.designationid), // e.g. "3"
value: String(item.designationid),
}));
setDesignation(formattedDesignation);
}, [data]);
const dropdownStyle = {
borderWidth: 1,
borderColor: colors.border,
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
backgroundColor: colors.white,
marginBottom: 10,
};
const renderInputWithIcon = (
icon: 'user' | 'phone' | 'mail',
placeholder: string,
value: string,
onChangeText: (text: string) => void,
onBlur: () => void,
error?: string,
touched?: boolean,
props: object = {},
) => {
const IconComponent = icon === 'user' ? User : icon === 'phone' ? Phone : Mail;
return (
<>
<View style={styles.inputWrapper}>
<IconComponent size={18} color={colors.grayText} style={styles.icon} />
<TextInput
placeholder={placeholder}
style={styles.input}
placeholderTextColor={colors.placeholder}
value={value}
onChangeText={onChangeText}
onBlur={onBlur}
{...props}
/>
</View>
{touched && error && <Text style={styles.errorText}>{error}</Text>}
</>
);
};
const renderPasswordInput = (
placeholder: string,
value: string,
onChangeText: (text: string) => void,
onBlur: () => void,
show: boolean,
setShow: (show: boolean) => void,
error?: string,
touched?: boolean,
) => (
<>
<View style={styles.inputWrapper}>
<Lock size={18} color={colors.grayText} style={styles.icon} />
<TextInput
placeholder={placeholder}
style={styles.input}
placeholderTextColor={colors.placeholder}
value={value}
onChangeText={onChangeText}
onBlur={onBlur}
secureTextEntry={!show}
/>
<TouchableOpacity onPress={() => setShow(!show)}>
{show ? <Eye size={18} color={colors.grayText} /> : <EyeOff size={18} color={colors.grayText} />}
</TouchableOpacity>
</View>
{touched && error && <Text style={styles.errorText}>{error}</Text>}
</>
);
const handleSubmitForm = (values: any) => {
console.log('values', values);
if (!isCheck) {
Alert.alert('Check terms and condition');
Alert.alert('Terms Required', 'Please accept the terms and conditions to proceed.');
return;
}
@@ -175,7 +103,7 @@ const Signup = () => {
mobNo: values.mobileNumber.trim(),
emailAdd: values.email.trim(),
contractorType: values.bidderType,
designation: values.designation ? Number(values.designation) : 0, // convert string -> number
designation: values.designation ? Number(values.designation) : 0,
password: values.password.trim(),
otherDesignation: null,
conPassword: values.confirmPassword.trim(),
@@ -187,7 +115,7 @@ const Signup = () => {
ToastAndroid.show(`${res.message}`, ToastAndroid.SHORT);
navigation.pop();
} else {
ToastAndroid.show(`Registration failed : ${res.message}`, ToastAndroid.SHORT);
ToastAndroid.show(`Registration failed: ${res.message}`, ToastAndroid.SHORT);
}
},
onError: (error: any) => {
@@ -197,21 +125,21 @@ const Signup = () => {
};
return (
<View style={styles.container}>
<SafeAreaView style={styles.container}>
<InsideHeader title="Sign Up" showArrow />
<View style={{ flex: 1, backgroundColor: colors.background }}>
<SafeAreaView style={{ flex: 1 }}>
<InsideHeader title="Create Account" showArrow />
<KeyboardAvoidingView
style={styles.container}
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
>
<Formik
initialValues={{
bidderType: '',
bidderType: 'Indian',
firstName: '',
middleName: '',
lastName: '',
designation: '', // string, not 0
designation: '',
mobileNumber: '',
email: '',
password: '',
@@ -232,174 +160,239 @@ const Signup = () => {
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
bounces={false}
showsVerticalScrollIndicator={false}
>
<Animated.View entering={FadeInUp.duration(450).delay(80)}>
<Text style={styles.label}>Bidder Type</Text>
<View style={styles.toggleContainer}>
{/* ═══ 1. Bidder Type Toggle ═══ */}
<Animated.View entering={FadeInUp.duration(400).delay(100)} style={{ marginBottom: spacing.m }}>
<View style={styles.sectionLabelRow}>
<UserPlus size={14} color={colors.primary} />
<AppText variant="caption" color={colors.primary} style={styles.sectionLabel}>
Bidder Type
</AppText>
</View>
<View style={[styles.toggleContainer, shadows.light]}>
{['Indian', 'Foreign'].map(type => (
<TouchableOpacity
key={type}
style={[
styles.toggleButton,
values.bidderType === type && styles.selectedToggle,
values.bidderType === type && [styles.toggleButtonActive, { backgroundColor: colors.primary }],
]}
onPress={() => setFieldValue('bidderType', type)}
activeOpacity={0.8}
>
<Text
style={[
styles.toggleText,
values.bidderType === type && styles.selectedToggleText,
]}
<AppText
variant="bodySmall"
color={values.bidderType === type ? colors.buttonText : colors.textSecondary}
style={{ fontWeight: values.bidderType === type ? '700' : '500' }}
>
{type}
</Text>
{type} Contractor
</AppText>
</TouchableOpacity>
))}
</View>
{touched.bidderType && errors.bidderType && (
<Text style={styles.errorText}>{errors.bidderType}</Text>
<AppText variant="caption" color={colors.error} style={styles.errorText}>
{errors.bidderType}
</AppText>
)}
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(120)}>
<View style={styles.row}>
<View style={styles.halfInput}>
{renderInputWithIcon(
'user',
'First Name *',
values.firstName,
handleChange('firstName'),
() => handleBlur('firstName'),
errors.firstName,
touched.firstName,
)}
</View>
<View style={styles.halfInput}>
{renderInputWithIcon(
'user',
'Middle Name',
values.middleName,
handleChange('middleName'),
() => handleBlur('middleName'),
errors.middleName,
touched.middleName,
)}
</View>
{/* ═══ 2. Personal Information ═══ */}
<Animated.View entering={FadeInUp.duration(450).delay(200)} style={[styles.card, shadows.medium]}>
<View style={styles.cardHeader}>
<User size={16} color={colors.primary} />
<AppText variant="bodySmall" color={colors.primary} style={styles.cardTitle}>
Personal Information
</AppText>
</View>
{renderInputWithIcon(
'user',
'Last Name *',
values.lastName,
handleChange('lastName'),
() => handleBlur('lastName'),
errors.lastName,
touched.lastName,
)}
</Animated.View>
<MyInput
label="First Name"
placeholder="Enter First Name"
value={values.firstName}
onChangeText={handleChange('firstName')}
onBlur={handleBlur('firstName')}
error={touched.firstName && errors.firstName ? errors.firstName : undefined}
rightIcon={<User size={18} color={colors.textSecondary} />}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
label="Middle Name"
placeholder="Enter Middle Name (Optional)"
value={values.middleName}
onChangeText={handleChange('middleName')}
onBlur={handleBlur('middleName')}
error={touched.middleName && errors.middleName ? errors.middleName : undefined}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
label="Last Name"
placeholder="Enter Last Name"
value={values.lastName}
onChangeText={handleChange('lastName')}
onBlur={handleBlur('lastName')}
error={touched.lastName && errors.lastName ? errors.lastName : undefined}
containerStyle={{ marginBottom: spacing.s }}
/>
<Animated.View entering={FadeInUp.duration(450).delay(160)}>
<Text style={styles.label}>Designation</Text>
<MyDropdown
label="Select Designation"
label="Designation"
labelField="label"
valueField="value"
data={designation}
value={values.designation}
onChange={(item: any) => {
setFieldValue('designation', item.value);
}}
placeholder={isDesignationLoading ? 'Loading...' : 'Choose Designation'}
style={dropdownStyle}
onChange={(item: any) => { setFieldValue('designation', item.value); }}
placeholder={isDesignationLoading ? 'Loading...' : 'Select Designation'}
/>
{touched.designation && errors.designation && (
<Text style={styles.errorText}>{errors.designation}</Text>
<AppText variant="caption" color={colors.error} style={styles.errorText}>
{errors.designation}
</AppText>
)}
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(200)}>
{renderInputWithIcon(
'phone',
'Mobile Number *',
values.mobileNumber,
handleChange('mobileNumber'),
() => handleBlur('mobileNumber'),
errors.mobileNumber,
touched.mobileNumber,
{
keyboardType: 'phone-pad',
maxLength: 10,
},
)}
{renderInputWithIcon(
'mail',
'Email *',
values.email,
handleChange('email'),
() => handleBlur('email'),
errors.email,
touched.email,
{
keyboardType: 'email-address',
autoCapitalize: 'none',
},
)}
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(240)}>
{renderPasswordInput(
'Password *',
values.password,
handleChange('password'),
() => handleBlur('password'),
showPassword,
setShowPassword,
errors.password,
touched.password,
)}
{renderPasswordInput(
'Confirm Password *',
values.confirmPassword,
handleChange('confirmPassword'),
() => handleBlur('confirmPassword'),
showConfirmPassword,
setShowConfirmPassword,
errors.confirmPassword,
touched.confirmPassword,
)}
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(280)}>
<View style={styles.termsRow}>
<TouchableOpacity onPress={() => setCheckModal(true)}>
{isCheck ? (
<CheckSquare size={20} color={colors.primary} />
) : (
<Square size={20} color={colors.black} />
)}
</TouchableOpacity>
<Text style={styles.termsText}>
By continuing, I agree to the Terms and Conditions
</Text>
{/* ═══ 3. Contact Details ═══ */}
<Animated.View entering={FadeInUp.duration(450).delay(300)} style={[styles.card, shadows.medium]}>
<View style={styles.cardHeader}>
<Contact size={16} color={colors.primary} />
<AppText variant="bodySmall" color={colors.primary} style={styles.cardTitle}>
Contact Details
</AppText>
</View>
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(320)} style={styles.buttonWrapper}>
<PrimaryButton
title={isRegistering ? 'Submitting...' : 'Submit'}
disabled={isRegistering}
onPress={() => handleSubmit()}
<MyInput
label="Mobile Number"
placeholder="Enter Mobile Number"
value={values.mobileNumber}
onChangeText={handleChange('mobileNumber')}
onBlur={handleBlur('mobileNumber')}
error={touched.mobileNumber && errors.mobileNumber ? errors.mobileNumber : undefined}
keyboardType="phone-pad"
maxLength={10}
rightIcon={<Phone size={18} color={colors.textSecondary} />}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
label="Email Address"
placeholder="Enter Email Address"
value={values.email}
onChangeText={handleChange('email')}
onBlur={handleBlur('email')}
error={touched.email && errors.email ? errors.email : undefined}
keyboardType="email-address"
autoCapitalize="none"
rightIcon={<Mail size={18} color={colors.textSecondary} />}
containerStyle={{ marginBottom: spacing.s }}
/>
</Animated.View>
{/* ═══ 4. Security ═══ */}
<Animated.View entering={FadeInUp.duration(450).delay(400)} style={[styles.card, shadows.medium]}>
<View style={styles.cardHeader}>
<Shield size={16} color={colors.primary} />
<AppText variant="bodySmall" color={colors.primary} style={styles.cardTitle}>
Security
</AppText>
</View>
<MyInput
label="Password"
placeholder="••••••••"
value={values.password}
onChangeText={handleChange('password')}
onBlur={handleBlur('password')}
secureTextEntry={!showPassword}
error={touched.password && errors.password ? errors.password : undefined}
rightIcon={
showPassword
? <Eye size={18} color={colors.textSecondary} />
: <EyeOff size={18} color={colors.textSecondary} />
}
rightIconPress={() => setShowPassword(prev => !prev)}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
label="Confirm Password"
placeholder="••••••••"
value={values.confirmPassword}
onChangeText={handleChange('confirmPassword')}
onBlur={handleBlur('confirmPassword')}
secureTextEntry={!showConfirmPassword}
error={touched.confirmPassword && errors.confirmPassword ? errors.confirmPassword : undefined}
rightIcon={
showConfirmPassword
? <Eye size={18} color={colors.textSecondary} />
: <EyeOff size={18} color={colors.textSecondary} />
}
rightIconPress={() => setShowConfirmPassword(prev => !prev)}
containerStyle={{ marginBottom: spacing.s }}
/>
</Animated.View>
{/* ═══ Terms & Conditions ═══ */}
<Animated.View entering={FadeInUp.duration(450).delay(500)}>
<TouchableOpacity
style={[styles.termsCard, shadows.light]}
onPress={() => setCheckModal(true)}
activeOpacity={0.8}
>
{isCheck ? (
<CheckSquare size={22} color={colors.primary} strokeWidth={2.5} />
) : (
<Square size={22} color={colors.textSecondary} strokeWidth={2} />
)}
<AppText variant="bodySmall" color={colors.textSecondary} style={{ flex: 1 }}>
I agree to the{' '}
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700', textDecorationLine: 'underline' }}>
Terms and Conditions
</AppText>
</AppText>
</TouchableOpacity>
</Animated.View>
{/* ═══ Submit Button ═══ */}
<Animated.View entering={FadeInDown.duration(400).delay(600)} style={styles.buttonWrapper}>
<PrimaryButton
title="Create Account"
loading={isRegistering}
onPress={handleSubmit}
variant="primary"
/>
</Animated.View>
{/* ═══ Login Link ═══ */}
<Animated.View entering={FadeInUp.duration(400).delay(650)}>
<TouchableOpacity
onPress={() => navigation.pop()}
style={styles.loginRow}
activeOpacity={0.7}
>
<AppText variant="bodySmall" color={colors.textSecondary}>
Already have an account?{' '}
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
Sign In
</AppText>
</AppText>
</TouchableOpacity>
</Animated.View>
{checkModal && (
<TermsAndConditionsModal
visible={checkModal}
onClose={() => setCheckModal(false)}
onChecked={() => setIsCheck(true)}
onChecked={() => { setIsCheck(true); setCheckModal(false); }}
/>
)}
</ScrollView>
@@ -413,99 +406,85 @@ const Signup = () => {
export default Signup;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.white,
},
// ── Styles ────────────────────────────────────────────────────────────
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
scrollContainer: {
flexGrow: 1,
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 24,
justifyContent: 'flex-start',
padding: spacing.m,
paddingBottom: 30,
},
label: {
fontSize: 15,
fontWeight: '600',
marginTop: 6,
marginBottom: 4,
color: colors.darkText,
sectionLabelRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
marginBottom: 6,
},
sectionLabel: {
fontWeight: '700',
fontSize: typography.size.xs,
letterSpacing: 0.5,
textTransform: 'uppercase',
},
toggleContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 4,
borderRadius: 12,
padding: 3,
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.border,
height: 46,
},
toggleButton: {
flex: 1,
paddingVertical: 10,
borderRadius: 10,
borderWidth: 1,
borderColor: colors.primary,
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 4,
backgroundColor: colors.white,
borderRadius: 9,
},
selectedToggle: {
backgroundColor: colors.primary,
borderColor: colors.primary,
toggleButtonActive: {
elevation: 2,
},
toggleText: {
fontSize: 14,
color: colors.grayText,
card: {
backgroundColor: colors.surface,
borderRadius: 14,
padding: 14,
marginBottom: spacing.m,
},
selectedToggleText: {
color: colors.white,
fontWeight: '600',
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 6,
},
halfInput: {
width: '48%',
},
inputWrapper: {
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 12,
borderWidth: 1,
borderColor: colors.border,
paddingHorizontal: 10,
marginTop: 8,
height: 52,
gap: 8,
marginBottom: spacing.s,
paddingBottom: spacing.xs,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
},
icon: {
marginRight: 10,
},
input: {
flex: 1,
fontSize: 15,
color: colors.darkText,
paddingVertical: 0,
cardTitle: {
fontWeight: '700',
fontSize: typography.size.s,
letterSpacing: 0.3,
},
errorText: {
color: colors.error,
fontSize: 12,
marginTop: 3,
marginLeft: 4,
marginTop: 4,
fontSize: typography.size.xs,
fontWeight: '500',
},
termsRow: {
termsCard: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 14,
marginHorizontal: 2,
},
termsText: {
flex: 1,
fontSize: 13,
color: colors.darkText,
marginLeft: 8,
gap: 10,
backgroundColor: colors.surface,
borderRadius: 12,
padding: 14,
marginBottom: spacing.m,
borderWidth: 1,
borderColor: colors.border,
},
buttonWrapper: {
marginTop: 18,
marginBottom: spacing.s,
},
loginRow: {
alignItems: 'center',
marginBottom: spacing.m,
},
});
+36 -60
View File
@@ -1,9 +1,8 @@
import React, { useEffect, useRef, useState, useCallback } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { Animated, StyleSheet, View } from 'react-native';
import { useNavigation, useIsFocused } from '@react-navigation/native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useSelector } from 'react-redux';
import { useStore } from 'react-redux'; // For hydration check
import { showToast } from '../../Constant/ShowDailog';
import { useRagistrationState } from '../../Services/ApiCall/Hooks';
@@ -24,37 +23,13 @@ const Splash: React.FC = () => {
const isFocused = useIsFocused();
const fadeAnim = useRef(new Animated.Value(0)).current;
const reduxToken = useSelector((state: any) => state.token?.token);
const store = useStore();
const isPersistRehydrated = useSelector((state: any) => state?._persist?.rehydrated);
const { mutateAsync: registrationStateMutate } = useRagistrationState();
const [isHydrated, setIsHydrated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const hasSessionToken = Boolean(reduxToken && typeof reduxToken === 'object' && reduxToken.token);
useEffect(() => {
let unsubscribe: any;
const checkHydration = () => {
const token = (store.getState() as any).token?.token;
if (token !== undefined) {
setIsHydrated(true);
unsubscribe?.();
}
};
unsubscribe = store.subscribe(checkHydration);
checkHydration(); // Check immediately
return () => unsubscribe?.();
}, []);
const routeByOrgType = (params: { otherId: any; orgType: any }) => {
// const orgType = params.orgType;
const orgType = "7" as string; // For testing purpose
const routeByOrgType = useCallback((params: { otherId: any; orgType: any }) => {
const orgType = String(params.orgType ?? '');
if (orgType === '4' || orgType === '5' || orgType === '6') {
navigation.replace(NavigationString.StageThreeScreen, params);
return;
}
@@ -65,28 +40,35 @@ const Splash: React.FC = () => {
}
navigation.replace('Dashboard');
};
}, [navigation]);
const handleGetData = async () => {
const handleGetData = useCallback(async () => {
try {
const res = await registrationStateMutate();
console.log("splash", res);
if (res?.status === false) {
navigation.reset({
index: 0,
routes: [{ name: NavigationString.Login }],
});
return;
}
if ( res?.data?.message==="No Response From Server"&& res?.data?.success===false){
navigation.reset({
index: 0,
routes: [{ name: NavigationString.Login }],
});
return
if (res?.data?.message === 'No Response From Server' && res?.data?.success === false) {
navigation.reset({
index: 0,
routes: [{ name: NavigationString.Login }],
});
return;
}
if (res?.statusCode === 404 || res?.statusCode === 401) {
navigation.replace('Registration');
return;
}
if (res?.statusCode === 404 || res?.statusCode === 401) {
navigation.replace('Registration');
return;
}
if (res?.statusCode === 200 && res?.data) {
const { registrationStatus, contOtherId, contOrganization } = res.data;
const params = { otherId: contOtherId, orgType: contOrganization };
switch (registrationStatus) {
case null:
case undefined:
@@ -115,18 +97,19 @@ const Splash: React.FC = () => {
if (error?.response?.status === 404) {
navigation.replace('Registration');
} else {
await AsyncStorage.removeItem('token');
navigation.replace('Signin');
await AsyncStorage.removeItem('persist:root');
navigation.replace(NavigationString.Login);
}
}
};
}, [navigation, registrationStateMutate, routeByOrgType]);
useEffect(() => {
if (!isFocused || !isHydrated) return;
if (!isFocused || !isPersistRehydrated) {
return;
}
const runSplash = async () => {
try {
setIsLoading(true);
Animated.timing(fadeAnim, {
toValue: 1,
duration: 500,
@@ -134,7 +117,7 @@ const Splash: React.FC = () => {
}).start();
if (!reduxToken.token) {
if (!hasSessionToken) {
navigation.reset({
index: 0,
routes: [{ name: NavigationString.Login }],
@@ -142,7 +125,7 @@ const Splash: React.FC = () => {
return;
}
if (reduxToken.userType == 'tender') {
if (reduxToken.userType === 'tender') {
await handleGetData();
} else {
navigation.reset({
@@ -154,18 +137,16 @@ const Splash: React.FC = () => {
} catch (error) {
navigation.reset({
index: 0,
routes: [{ name: NavigationString.Login || 'Signin' }],
routes: [{ name: NavigationString.Login }],
});
} finally {
setIsLoading(false);
}
};
const timeoutId = setTimeout(runSplash, 200);
return () => clearTimeout(timeoutId);
}, [isFocused, isHydrated]);
}, [fadeAnim, handleGetData, hasSessionToken, isFocused, isPersistRehydrated, navigation, reduxToken?.userType]);
if (!isHydrated) {
if (!isPersistRehydrated) {
return (
<View style={styles.loadingContainer}>
<Animated.Image
@@ -185,11 +166,6 @@ const Splash: React.FC = () => {
style={[styles.logo, { opacity: fadeAnim }]}
resizeMode="contain"
/>
{isLoading && (
<View style={{ marginTop: 20 }}>
{/* Optional spinner */}
</View>
)}
</View>
</View>
);
@@ -21,16 +21,16 @@ import { encrypt, encryptWithRSA } from '../../../Services/Encryption/encrypt';
/* -------------------- UTILS -------------------- */
interface OfficerKey {
officersId: number;
officerPublicKey: string;
officerType: string;
officersId: number;
officerPublicKey: string;
officerType: string;
}
interface SaveEnvelopeOfficerDetail {
officerId: number;
officerEncryptedKey: string;
officerType: string;
officerId: number;
officerEncryptedKey: string;
officerType: string;
}
// Convert ArrayBuffer → Base64
@@ -61,7 +61,7 @@ const isTotalRow = (row: any) =>
const EnvelopeCExcelPreview = (props: any) => {
const tenderData = props;
console.log("Tender Data",tenderData)
console.log("Tender Data", tenderData)
const token = useSelector((state: any) => state.token.token);
const navigation = useNavigation() as any;
@@ -146,8 +146,8 @@ const EnvelopeCExcelPreview = (props: any) => {
if (isTotalRow(row)) return;
const qtyKey:any = findColumnKey(columns, ['qty', 'quantity']);
const rateKey:any = findColumnKey(columns, ['rate', 'price']);
const qtyKey: any = findColumnKey(columns, ['qty', 'quantity']);
const rateKey: any = findColumnKey(columns, ['rate', 'price']);
const valueKey = findColumnKey(columns, ['value', 'amount', 'total']);
@@ -253,7 +253,7 @@ const EnvelopeCExcelPreview = (props: any) => {
// Encrypt the entire file content
console.log('🔒 Encrypting file content...');
const encryptedContent:any = encrypt(fileContent, encryptionKey);
const encryptedContent: any = encrypt(fileContent, encryptionKey);
console.log('✅ File content encrypted, Encrypted length:', encryptedContent?.length);
@@ -353,6 +353,7 @@ const EnvelopeCExcelPreview = (props: any) => {
deviceType: 'MOBILE',
fileName: 'envelopeC_encrypted.xlsx',
saveEnvelopesOfficersDetailsResDTO: officerEncryptedKeys,
randomKeyGenerated: ENCRYPTION_KEY,
};
@@ -375,7 +376,7 @@ const EnvelopeCExcelPreview = (props: any) => {
console.log('\n📝 STEP 6: Uploading encrypted file to server');
console.log('───────────────────────────────────────────────────────');
console.log('FormData prepared:', formData);
const response = await axios.post(SUBMIT_ENVELOPE_C_UPLOAD, formData, {
headers: {
+194
View File
@@ -0,0 +1,194 @@
import React, { useState } from 'react';
import { View, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { InsideHeader } from '../../Component/Index';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
import { useNavigation, useRoute } from '@react-navigation/native';
import { AlertCircle, CheckCircle } from 'lucide-react-native';
import DeviceInfo from 'react-native-device-info';
import { apiCall } from '../../Axios/Axios';
import { BASE_URL } from '../../Services/ApiCall/ApiUrls';
import Geolocation from 'react-native-geolocation-service';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { Platform } from 'react-native';
import { clampRGBA } from 'react-native-reanimated/lib/typescript/Colors';
const DeviceBinding = () => {
const { colors, spacing, typography } = useTheme();
const navigation = useNavigation<any>();
const route = useRoute<any>();
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const { message, showBindButton } = route.params || {
message: "Device is not mapped or not active. Please register and activate your device.",
showBindButton: false
};
const getLocation = (): Promise<{ latitude: string, longitude: string }> => {
return new Promise(async (resolve) => {
try {
const permission = Platform.OS === 'ios'
? PERMISSIONS.IOS.LOCATION_WHEN_IN_USE
: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION;
const result = await request(permission);
if (result === RESULTS.GRANTED) {
Geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude.toString(),
longitude: position.coords.longitude.toString(),
});
},
(error) => {
console.log("Location Error: ", error);
resolve({ latitude: "23.77", longitude: "77.34" }); // Fallback to standard
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
);
} else {
resolve({ latitude: "23.77", longitude: "77.34" }); // Fallback
}
} catch (err) {
console.log("Permission Error: ", err);
resolve({ latitude: "23.77", longitude: "77.34" }); // Fallback
}
});
};
const handleBindDevice = async () => {
setLoading(true);
try {
const deviceId = await DeviceInfo.getUniqueId();
const systemName = DeviceInfo.getSystemName();
const systemVersion = DeviceInfo.getSystemVersion();
const deviceOs = `${systemName} ${systemVersion}`;
const lastIp = await DeviceInfo.getIpAddress();
const brand = DeviceInfo.getBrand();
const model = DeviceInfo.getModel();
const deviceName = `${brand} ${model}`;
const browser = "Mobile App";
const { latitude, longitude } = await getLocation();
const location = `${longitude},${latitude}`;
const payload = {
browser,
deviceOs,
deviceId,
lastIp,
deviceName,
location,
};
const response: any = await apiCall('post', `${BASE_URL}tenderBidding/deviceBinding`, payload);
if (response?.status === 'SUCCESS' || response?.statusCode === 200) {
setSuccess(true);
// Optionally navigate back after a delay
setTimeout(() => {
navigation.goBack();
}, 2000);
}
} catch (error) {
console.log("Error binding device: ", error);
} finally {
setLoading(false);
}
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.l,
},
iconContainer: {
marginBottom: spacing.l,
},
title: {
marginBottom: spacing.m,
textAlign: 'center',
},
message: {
textAlign: 'center',
marginBottom: spacing.xl,
lineHeight: 24,
},
button: {
backgroundColor: colors.primary,
paddingVertical: spacing.m,
paddingHorizontal: spacing.xl,
borderRadius: 12,
width: '100%',
alignItems: 'center',
marginBottom: spacing.m,
},
secondaryButton: {
backgroundColor: 'transparent',
paddingVertical: spacing.m,
width: '100%',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.border,
borderRadius: 12,
}
});
return (
<View style={styles.container}>
<InsideHeader title="Device Binding" showArrow />
<View style={styles.content}>
<View style={styles.iconContainer}>
{success ? (
<CheckCircle size={64} color={colors.success} />
) : (
<AlertCircle size={64} color={colors.error} />
)}
</View>
<AppText variant="h3" color={colors.text} style={styles.title}>
{success ? "Binding Successful" : "Device Not Bound"}
</AppText>
<AppText variant="body" color={colors.textSecondary} style={styles.message}>
{success ? "Your device binding request has been submitted successfully." : message}
</AppText>
{showBindButton && !success && (
<TouchableOpacity
style={styles.button}
onPress={handleBindDevice}
disabled={loading}
>
{loading ? (
<ActivityIndicator size="small" color={colors.buttonText} />
) : (
<AppText variant="button" color={colors.buttonText}>
Bind This Device
</AppText>
)}
</TouchableOpacity>
)}
<TouchableOpacity
style={success ? styles.button : styles.secondaryButton}
onPress={() => navigation.goBack()}
>
<AppText variant="button" color={success ? colors.buttonText : colors.text}>
{success ? "Finish" : "Go Back"}
</AppText>
</TouchableOpacity>
</View>
</View>
);
};
export default DeviceBinding;
+79
View File
@@ -0,0 +1,79 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import { InsideHeader } from '../../Component/Index';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
import { useNavigation, useRoute } from '@react-navigation/native';
import { AlertCircle } from 'lucide-react-native';
const DscBinding = () => {
const { colors, spacing, typography } = useTheme();
const navigation = useNavigation<any>();
const route = useRoute<any>();
// Fallback message just in case
const message = route.params?.message || "DSC is not mapped. Please map your DSC on the web portal before proceeding.";
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.l,
},
iconContainer: {
marginBottom: spacing.l,
},
title: {
marginBottom: spacing.m,
textAlign: 'center',
},
message: {
textAlign: 'center',
marginBottom: spacing.xl,
lineHeight: 24,
},
button: {
backgroundColor: colors.primary,
paddingVertical: spacing.m,
paddingHorizontal: spacing.xl,
borderRadius: 12,
width: '100%',
alignItems: 'center',
}
});
return (
<View style={styles.container}>
<InsideHeader title="DSC Binding" showArrow />
<View style={styles.content}>
<View style={styles.iconContainer}>
<AlertCircle size={64} color={colors.error} />
</View>
<AppText variant="h3" color={colors.text} style={styles.title}>
DSC Not Bound
</AppText>
<AppText variant="body" color={colors.textSecondary} style={styles.message}>
{message}
</AppText>
<TouchableOpacity
style={styles.button}
onPress={() => navigation.goBack()}
>
<AppText variant="button" color={colors.buttonText}>
Go Back
</AppText>
</TouchableOpacity>
</View>
</View>
);
};
export default DscBinding;
+33 -11
View File
@@ -1,18 +1,19 @@
import React, { useCallback, useState, useEffect } from 'react';
import { View, FlatList, ActivityIndicator, RefreshControl, Text } from 'react-native';
import { View, FlatList, ActivityIndicator, RefreshControl, Text, StyleSheet } from 'react-native';
import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated';
import { useNavigation } from '@react-navigation/native';
import { useFetchLiveAuction, useFetchLiveTender } from '../../Services/ApiCall/Hooks';
import { SearchInput } from '../../Component/Serach/SeacrhInput';
import { Skelton } from '../../Component/Index';
import colors from '../../Constant/Color';
import { useDispatch, useSelector } from 'react-redux';
import { togglePurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender';
import LiveTenderTile from './LiveTenderTile';
import { useTheme } from '../../Hooks/useTheme'; // Using Theme
const LiveTender = () => {
const navigation: any = useNavigation();
const Dispatch = useDispatch()
const { colors } = useTheme(); // Hook
const user = useSelector((state: any) => state?.token.token);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch }: any = useFetchLiveTender();
@@ -47,7 +48,7 @@ const LiveTender = () => {
const renderItem = useCallback(
({ item, index }: any) => {
if(item.tenderStatus==="Cancelled") return null;
if (item.tenderStatus === "Cancelled") return null;
const isPurchase = SelectedPurchaseList.findIndex((prev: any) => prev.tenderId === item.tenderId)
return <LiveTenderTile
contractorId={user?.id}
@@ -68,11 +69,20 @@ const LiveTender = () => {
);
if (isLoading) {
return <Skelton />;
return <View style={{ flex: 1, backgroundColor: colors.background }}>
<Skelton />
</View>;
}
return (
<View style={{ flex: 1 }}>
<SearchInput onChange={() => { }} placeholder="Search...." />
<View style={{ flex: 1, backgroundColor: colors.background }}>
{/* Liquid Background Blobs */}
<View style={styles.blobContainer}>
<View style={[styles.blob, { top: -50, right: -50, backgroundColor: colors.primary, opacity: 0.05 }]} />
<View style={[styles.blob, { top: 200, left: -100, backgroundColor: colors.secondary, opacity: 0.05 }]} />
<View style={[styles.blob, { bottom: -50, right: -20, backgroundColor: colors.primary, opacity: 0.05 }]} />
</View>
<SearchInput onChange={() => { }} placeholder="Search Tenders..." filter={false} />
{showRefreshBanner && (
<Animated.View
@@ -87,7 +97,7 @@ const LiveTender = () => {
paddingHorizontal: 14,
marginTop: 6,
borderRadius: 20,
backgroundColor: '#FFFFFF',
backgroundColor: colors.surface,
shadowColor: '#000',
shadowOpacity: 0.06,
shadowRadius: 8,
@@ -102,7 +112,7 @@ const LiveTender = () => {
<Text
style={{
fontSize: 13,
color: '#374151',
color: colors.text,
fontWeight: '500',
}}
>
@@ -124,7 +134,7 @@ const LiveTender = () => {
<RefreshControl
refreshing={isRefetching}
onRefresh={refetch}
tintColor="transparent"
tintColor={colors.primary}
colors={[colors.primary]}
/>
}
@@ -133,11 +143,23 @@ const LiveTender = () => {
windowSize={7}
removeClippedSubviews
updateCellsBatchingPeriod={50}
onRefresh={refetch}
refreshing={isRefetching}
/>
</View>
);
};
const styles = StyleSheet.create({
blobContainer: {
...StyleSheet.absoluteFillObject,
zIndex: -1,
overflow: 'hidden',
},
blob: {
position: 'absolute',
width: 300,
height: 300,
borderRadius: 150,
},
});
export default LiveTender;
+220 -196
View File
@@ -1,20 +1,15 @@
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import {
Alert,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
StatusBar,
SafeAreaView,
} from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native';
import {
useFetchLiveTenderById,
} from '../../Services/ApiCall/Hooks';
import { InsideHeader } from '../../Component/Index';
import colors from '../../Constant/Color';
import ReactNativeBlobUtil from 'react-native-blob-util';
import { GET_DOCID } from '../../Services/ApiCall/ApiUrls';
import {
@@ -27,20 +22,23 @@ import {
} from 'lucide-react-native';
import moment from 'moment';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { showSuccessAlert, showToast } from '../../Constant/ShowDailog';
import { showToast } from '../../Constant/ShowDailog';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
const InfoRow = ({ label, value }: any) => (
const InfoRow = ({ label, value, styles }: any) => (
<View style={styles.row}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.value}>{value || '-'}</Text>
<AppText variant="caption" style={styles.label}>{label}</AppText>
<AppText variant="bodySmall" style={styles.value}>{value || '-'}</AppText>
</View>
);
const Section = ({ title, icon: Icon, children }: any) => (
const Section = ({ title, icon: Icon, children, styles, colors }: any) => (
<View style={styles.sectionContainer}>
<View style={styles.sectionHeader}>
<Icon size={18} color={colors.primary} />
<Text style={styles.sectionTitle}>{title}</Text>
<AppText variant="body" style={styles.sectionTitle}>{title}</AppText>
</View>
{children}
</View>
@@ -48,11 +46,156 @@ const Section = ({ title, icon: Icon, children }: any) => (
const LiveTenderDetail = () => {
const route = useRoute<any>();
const { colors, spacing, shadows, isDarkMode } = useTheme();
const navigation = useNavigation<any>();
const tender = route?.params;
const { data } = useFetchLiveTenderById(tender?.tenderId);
const [downloadingId, setDownloadingId] = useState<string | number | null>(null);
const insets = useSafeAreaInsets()
const styles = useMemo(() => StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: colors.background,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 10,
borderBottomWidth: 1,
borderBottomColor: colors.border,
backgroundColor: colors.surface,
paddingVertical: 12,
},
backBtn: {
padding: 8,
marginRight: 8,
},
headerTitle: {
fontSize: 18,
fontWeight: '800',
color: colors.text,
},
headerSub: {
fontSize: 13,
color: colors.textSecondary,
},
liveBox: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
borderWidth: 1,
borderColor: colors.border,
backgroundColor: colors.background,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
},
liveText: {
fontSize: 11,
fontWeight: '800',
color: colors.textSecondary,
},
content: {
padding: 16,
paddingBottom: 32,
},
sectionContainer: {
backgroundColor: colors.surface,
borderRadius: 16,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: colors.border,
...shadows.light,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
marginBottom: 12,
paddingBottom: 6,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
sectionTitle: {
fontSize: 14,
fontWeight: '800',
color: colors.text,
textTransform: 'uppercase',
letterSpacing: 0.4,
},
card: {
backgroundColor: colors.surface,
borderRadius: 12,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
paddingVertical: 8,
borderBottomWidth: 0.5,
borderBottomColor: colors.border,
},
label: {
fontSize: 13,
color: colors.textSecondary,
flex: 1,
paddingRight: 8,
fontWeight: '600',
},
value: {
fontSize: 13,
color: colors.text,
flex: 1,
textAlign: 'right',
fontWeight: '500',
},
documentRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
gap: 10,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
docInfo: {
flexDirection: 'row',
alignItems: 'center',
flex: 1.4,
},
fileIcon: {
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: colors.primary + '20', // Opacity
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
},
docName: {
flex: 1,
color: colors.text,
fontSize: 13,
fontWeight: '600',
},
downloadBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
downloadText: {
fontSize: 13,
fontWeight: '700',
color: colors.primary,
},
}), [colors, shadows]);
if (!data?.data?.[0]) return null;
const tenderData = data.data[0];
@@ -77,7 +220,7 @@ const LiveTenderDetail = () => {
}).fetch('GET', downloadUrl, {
Accept: 'application/pdf',
});
const stat = await ReactNativeBlobUtil.fs.stat(path);
// const stat = await ReactNativeBlobUtil.fs.stat(path);
showToast('Download complete');
setDownloadingId(null);
} catch (error: any) {
@@ -98,193 +241,214 @@ const LiveTenderDetail = () => {
};
return (
<View style={styles.safeArea}>
<StatusBar barStyle="dark-content" />
<View style={[styles.header,{marginTop:insets.top}]}>
<View style={[styles.safeArea, { backgroundColor: colors.background }]}>
<View style={[styles.header, { marginTop: insets.top }]}>
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backBtn}
>
<ArrowLeft size={24} color="#1e293b" />
<ArrowLeft size={24} color={colors.text} />
</TouchableOpacity>
<View style={{ flex: 1 }}>
<Text style={styles.headerTitle}>Tender Overview</Text>
<Text style={styles.headerSub}>
<AppText variant="body" style={styles.headerTitle}>Tender Overview</AppText>
<AppText variant="caption" style={styles.headerSub}>
Ref: {tenderData?.tenderNumber || '-'}
</Text>
</AppText>
</View>
<View style={styles.liveBox}>
<View
style={[
styles.dot,
{ backgroundColor: isLive ? '#22c55e' : '#94a3b8' },
{ backgroundColor: isLive ? colors.success : colors.textSecondary },
]}
/>
<Text style={styles.liveText}>{isLive ? 'LIVE' : 'CLOSED'}</Text>
<AppText variant="bodySmall" style={styles.liveText}>{isLive ? 'LIVE' : 'CLOSED'}</AppText>
</View>
</View>
<ScrollView contentContainerStyle={styles.content}>
<Section title="Basic Details" icon={Layers}>
<Section title="Basic Details" icon={Layers} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow label="Tender ID" value={tenderData?.tenderId} />
<InfoRow label="Tender Number" value={tenderData?.tenderNumber} />
<InfoRow label="NIT Number" value={tenderData?.nitNumber} />
<InfoRow label="NIT Date" value={tenderData?.nitDate} />
<InfoRow label="Name of Work" value={tenderData?.nameOfWork} />
<InfoRow label="Description" value={tenderData?.descriptionOfWork} />
<InfoRow label="Tender Stage" value={tenderData?.tenderStage} />
<InfoRow label="Tender Status" value={tenderData?.tenderStatus} />
<InfoRow label="Tender ID" value={tenderData?.tenderId} styles={styles} />
<InfoRow label="Tender Number" value={tenderData?.tenderNumber} styles={styles} />
<InfoRow label="NIT Number" value={tenderData?.nitNumber} styles={styles} />
<InfoRow label="NIT Date" value={tenderData?.nitDate} styles={styles} />
<InfoRow label="Name of Work" value={tenderData?.nameOfWork} styles={styles} />
<InfoRow label="Description" value={tenderData?.descriptionOfWork} styles={styles} />
<InfoRow label="Tender Stage" value={tenderData?.tenderStage} styles={styles} />
<InfoRow label="Tender Status" value={tenderData?.tenderStatus} styles={styles} />
</View>
</Section>
<Section title="Category & Type" icon={Briefcase}>
<Section title="Category & Type" icon={Briefcase} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow label="Tender Category" value={tenderData?.tenderCategory} />
<InfoRow label="Type of Tender" value={tenderData?.typeOfTender} />
<InfoRow label="Selective Tender" value={tenderData?.selectiveTender} />
<InfoRow label="Area of Work" value={tenderData?.areaOfWork} />
<InfoRow label="Tender Category" value={tenderData?.tenderCategory} styles={styles} />
<InfoRow label="Type of Tender" value={tenderData?.typeOfTender} styles={styles} />
<InfoRow label="Selective Tender" value={tenderData?.selectiveTender} styles={styles} />
<InfoRow label="Area of Work" value={tenderData?.areaOfWork} styles={styles} />
</View>
</Section>
<Section title="Financial Details" icon={FileText}>
<Section title="Financial Details" icon={FileText} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow
label="Estimated Value"
value={tenderData?.estimateValueInFig ? `₹ ${tenderData.estimateValueInFig}` : '-'}
styles={styles}
/>
<InfoRow
label="Estimated Value (Words)"
value={tenderData?.estimateValueInWord}
styles={styles}
/>
<InfoRow
label="Tender Fee"
value={tenderData?.tenderFeeInFig ? `₹ ${tenderData.tenderFeeInFig}` : '-'}
styles={styles}
/>
<InfoRow
label="EMD"
value={tenderData?.emdInFig ? `₹ ${tenderData.emdInFig}` : '-'}
styles={styles}
/>
<InfoRow label="GST %" value={tenderData?.gst} />
<InfoRow label="GST Amount" value={tenderData?.gstAmmount} />
<InfoRow label="GST %" value={tenderData?.gst} styles={styles} />
<InfoRow label="GST Amount" value={tenderData?.gstAmmount} styles={styles} />
</View>
</Section>
<Section title="Work & Validity" icon={Briefcase}>
<Section title="Work & Validity" icon={Briefcase} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow
label="Bid Validity Type"
value={tenderData?.bidValidateType}
styles={styles}
/>
<InfoRow
label="Bid Validity Period"
value={tenderData?.bidValidatePeriod}
styles={styles}
/>
<InfoRow
label="Work Completion Type"
value={tenderData?.workCompletionType}
styles={styles}
/>
<InfoRow
label="Work Completion Period"
value={tenderData?.workCompletionPeriod}
styles={styles}
/>
<InfoRow label="Rainy Season" value={tenderData?.rainySeason} />
<InfoRow label="Rainy Season" value={tenderData?.rainySeason} styles={styles} />
</View>
</Section>
<Section title="Key Dates" icon={Calendar}>
<Section title="Key Dates" icon={Calendar} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow
label="Purchase Start"
value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.tenderPurchaseDate?.startDateTime : tenderData?.keydates?.tenderPurchaseDate?.startDateTime
)}
styles={styles}
/>
<InfoRow
label="Purchase End"
value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.tenderPurchaseDate?.endDateTime : tenderData?.keydates?.tenderPurchaseDate?.endDateTime
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.tenderPurchaseDate?.endDateTime : tenderData?.keydates?.tenderPurchaseDate?.endDateTime
)}
styles={styles}
/>
<InfoRow
label="Bid Submission Start"
value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.startDateTime : tenderData?.keydates?.bidSubmissionDate?.startDateTime
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.startDateTime : tenderData?.keydates?.bidSubmissionDate?.startDateTime
)}
styles={styles}
/>
<InfoRow
label="Bid Submission End"
value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.endDateTime : tenderData?.keydates?.bidSubmissionDate?.endDateTime,
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.endDateTime : tenderData?.keydates?.bidSubmissionDate?.endDateTime,
)}
styles={styles}
/>
<InfoRow
label="Opening Envelope A"
value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEA : tenderData?.keydates?.openingEA
)}
styles={styles}
/>
<InfoRow
label="Opening Envelope B"
value={formatDateTime(tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEB : tenderData?.keydates?.openingEB)}
styles={styles}
/>
<InfoRow
label="Opening Envelope C"
value={formatDateTime(tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEC : tenderData?.keydates?.openingEC)}
styles={styles}
/>
</View>
</Section>
<Section title="Envelope Configuration" icon={Layers}>
<Section title="Envelope Configuration" icon={Layers} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow
label="Envelope Type"
value={tenderData?.envelopeType}
styles={styles}
/>
<InfoRow
label="Envelope A Enabled"
value={tenderData?.envelopeA ? 'Yes' : 'No'}
styles={styles}
/>
<InfoRow
label="Envelope B Enabled"
value={tenderData?.envelopeB ? 'Yes' : 'No'}
styles={styles}
/>
<InfoRow
label="Envelope C Enabled"
value={tenderData?.envelopeC ? 'Yes' : 'No'}
styles={styles}
/>
</View>
</Section>
<Section title="Department & Location" icon={Layers}>
<Section title="Department & Location" icon={Layers} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow label="Department" value={tenderData?.deptName} />
<InfoRow label="Circle" value={tenderData?.circle} />
<InfoRow label="Department" value={tenderData?.deptName} styles={styles} />
<InfoRow label="Circle" value={tenderData?.circle} styles={styles} />
<InfoRow
label="Division"
value={tenderData?.tenderSelectedDivision}
styles={styles}
/>
<InfoRow
label="Sub Division"
value={tenderData?.tenderSelectedSubDivision}
styles={styles}
/>
</View>
</Section>
<Section title="Officers" icon={Layers}>
<Section title="Officers" icon={Layers} styles={styles} colors={colors}>
<View style={styles.card}>
<InfoRow label="Tender Master" value={tenderData?.tenderMaster} />
<InfoRow label="Tender Master" value={tenderData?.tenderMaster} styles={styles} />
{tenderData?.approvalOfficerNameEmail?.map((o: any, i: number) => (
<InfoRow
key={i}
label={`Approval Officer ${i + 1}`}
value={`${o.name} (${o.email})`}
styles={styles}
/>
))}
</View>
</Section>
<Section title="Documents" icon={Download}>
<Section title="Documents" icon={Download} styles={styles} colors={colors}>
<View style={styles.card}>
{tenderData?.tenderDocuments?.map((doc: any, i: number) => (
<View key={i} style={styles.documentRow}>
@@ -292,7 +456,7 @@ const LiveTenderDetail = () => {
<View style={styles.fileIcon}>
<FileText size={18} color={colors.primary} />
</View>
<Text style={styles.docName}>{doc.fileName}</Text>
<AppText variant="bodySmall" style={styles.docName}>{doc.fileName}</AppText>
</View>
<TouchableOpacity
style={styles.downloadBtn}
@@ -302,12 +466,12 @@ const LiveTenderDetail = () => {
<Download
size={18}
color={
downloadingId === doc?.docId ? '#9ca3af' : colors.primary
downloadingId === doc?.docId ? colors.textSecondary : colors.primary
}
/>
<Text style={styles.downloadText}>
<AppText variant="bodySmall" style={styles.downloadText}>
{downloadingId === doc?.docId ? 'Downloading...' : 'Download'}
</Text>
</AppText>
</TouchableOpacity>
</View>
))}
@@ -319,143 +483,3 @@ const LiveTenderDetail = () => {
};
export default LiveTenderDetail;
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#f8fafc',
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal:10,
borderBottomWidth: 1,
borderBottomColor: '#f1f5f9',
backgroundColor: '#fff',
},
backBtn: {
padding: 8,
marginRight: 8,
},
headerTitle: {
fontSize: 18,
fontWeight: '800',
color: '#0f172a',
},
headerSub: {
fontSize: 13,
color: '#64748b',
},
liveBox: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
borderWidth: 1,
borderColor: '#e2e8f0',
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
},
liveText: {
fontSize: 11,
fontWeight: '800',
color: '#475569',
},
content: {
padding: 16,
paddingBottom: 32,
},
sectionContainer: {
backgroundColor: '#ffffff',
borderRadius: 16,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: '#e2e8f0',
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
marginBottom: 12,
paddingBottom: 6,
borderBottomWidth: 1,
borderBottomColor: '#f8fafc',
},
sectionTitle: {
fontSize: 14,
fontWeight: '800',
color: '#1e293b',
textTransform: 'uppercase',
letterSpacing: 0.4,
},
card: {
backgroundColor: '#ffffff',
borderRadius: 12,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
paddingVertical: 8,
borderBottomWidth: 0.5,
borderBottomColor: '#e5e7eb',
},
label: {
fontSize: 13,
color: '#64748b',
flex: 1,
paddingRight: 8,
fontWeight: '600',
},
value: {
fontSize: 13,
color: '#111827',
flex: 1,
textAlign: 'right',
fontWeight: '500',
},
documentRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
gap: 10,
borderBottomWidth: 1,
borderBottomColor: '#f1f5f9',
},
docInfo: {
flexDirection: 'row',
alignItems: 'center',
flex: 1.4,
},
fileIcon: {
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: '#e0f2fe',
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
},
docName: {
flex: 1,
color: '#111827',
fontSize: 13,
fontWeight: '600',
},
downloadBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
downloadText: {
fontSize: 13,
fontWeight: '700',
color: colors.primary,
},
});
+155 -163
View File
@@ -1,13 +1,16 @@
import React, { memo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { View, StyleSheet, TouchableOpacity, Dimensions } from 'react-native';
import moment from 'moment';
import colors from '../../Constant/Color';
import { LiveTenderTileProps } from './types/types';
import { Calendar, Tag, Info, ArrowRight } from 'lucide-react-native';
import TenderDetail from './Modal/TenderFullDetail';
import TenderFullDetail from './Modal/TenderFullDetail';
import { Calendar, Tag, Info, ArrowRight, Clock, MapPin } from 'lucide-react-native';
import { useNavigation } from '@react-navigation/native';
import { format } from '../../Helper/DateFormat';
import { useTheme } from '../../Hooks/useTheme';
import LinearGradient from 'react-native-linear-gradient';
import AppText from '../../components/AppText';
import NavigationString from '../../Constant/NavigationString';
const { width } = Dimensions.get('window');
const LiveTenderTile: React.FC<LiveTenderTileProps> = ({
contractorId,
@@ -21,6 +24,8 @@ const LiveTenderTile: React.FC<LiveTenderTileProps> = ({
tenderNumber,
}) => {
const navigation: any = useNavigation();
const { colors, shadows, spacing } = useTheme();
const purchaseEnded = moment().isAfter(moment(purchaseEnd, 'YYYY-MM-DD HH:mm:ss'));
const contractorIds = typeof data?.contractorId === 'string'
@@ -30,227 +35,214 @@ const LiveTenderTile: React.FC<LiveTenderTileProps> = ({
: [];
const purchased = contractorIds.includes(String(contractorId));
if(data.tenderStatus==="Amended"){
}
const getStatusColor = () => {
if (isPurchased) return colors.error;
if (purchaseEnded) return colors.textSecondary;
if (purchased) return colors.textSecondary;
return colors.success;
};
const statusColor = getStatusColor();
return (
<View style={styles.cardWrapper}>
{/* Status Tag */}
<View
style={[
styles.statusTag,
{ backgroundColor: isPurchased ? colors.error : purchaseEnded ? '#94a3b8' : (purchased ? '#94a3b8' : '#4ade80') },
]}
/>
<TouchableOpacity
activeOpacity={0.9}
onPress={() => navigation.navigate('LiveTenderDetail', data)}
style={[styles.container, {
backgroundColor: colors.surface,
shadowColor: colors.shadow,
borderColor: colors.border
}]}
>
<TouchableOpacity
style={styles.container}
activeOpacity={0.9}
onPress={() => navigation.navigate('LiveTenderDetail', data)}
>
{/* Header */}
<View style={styles.header}>
<View style={styles.badge}>
<View style={styles.contentContainer}>
{/* Header Section */}
<View style={styles.headerRow}>
<View style={[styles.idTag, { backgroundColor: colors.background }]}>
<Tag size={14} color={colors.primary} />
<Text style={styles.badgeText}>#{tenderNumber}</Text>
<AppText variant="caption" style={{ color: colors.primary, fontWeight: '700' }}>
#{tenderNumber}
</AppText>
</View>
<Text style={styles.priceText}>₹{tenderFees}</Text>
<AppText variant="body" style={{ color: colors.primary }}>
₹{tenderFees}
</AppText>
</View>
{/* Work Title */}
<Text style={styles.workTitle} numberOfLines={2}>
{/* Title Section */}
<AppText
variant="body"
style={[styles.title, { color: colors.text }]}
numberOfLines={2}
>
{tenderOfWork ?? 'General Tender Work'}
</Text>
</AppText>
{/* Info Grid */}
<View style={styles.infoGrid}>
<View style={styles.infoItem}>
<Calendar size={16} color="#64748b" />
<View style={styles.infoTextGroup}>
<Text style={styles.infoLabel}>Start</Text>
<Text style={styles.infoValue}>{format.date6(`${tenderDispatchDate}`)}</Text>
<View style={[styles.infoContainer, { backgroundColor: colors.background }]}>
<View style={styles.infoRow}>
<View style={styles.infoCol}>
<View style={styles.iconBox}>
<Calendar size={16} color={colors.textSecondary} />
</View>
<View>
<AppText variant="caption" style={{ color: colors.textSecondary }}>Start Date</AppText>
<AppText variant='caption' style={{ color: colors.text, fontWeight: '600' }}>
{format.date6(`${tenderDispatchDate}`)}
</AppText>
</View>
</View>
</View>
<View style={styles.infoItem}>
<Calendar size={16} color={purchaseEnded ? "#ef4444" : "#22c55e"} />
<View style={styles.infoTextGroup}>
<Text style={styles.infoLabel}>Deadline</Text>
<Text style={[styles.infoValue, purchaseEnded && { color: '#ef4444' }]}>
{moment(purchaseEnd, 'YYYY-MM-DD HH:mm:ss').format('DD MMM, YY')}
</Text>
<View style={styles.divider} />
<View style={[styles.infoCol, { alignItems: 'flex-end' }]}>
<View style={styles.iconBox}>
<Clock size={16} color={purchaseEnded ? colors.error : colors.success} />
</View>
<View style={{ alignItems: 'flex-end' }}>
<AppText variant="caption" style={{ color: colors.textSecondary }}>Deadline</AppText>
<AppText variant="caption" style={{ color: purchaseEnded ? colors.error : colors.text, fontWeight: '600' }}>
{moment(purchaseEnd, 'YYYY-MM-DD HH:mm:ss').format('DD MMM, YY')}
</AppText>
</View>
</View>
</View>
</View>
{/* Action Bar */}
<View style={styles.actionBar}>
<TouchableOpacity style={styles.iconBtn} onPress={() => navigation.navigate('LiveTenderDetail', data)}>
<Info size={18} color={colors.primary} />
<Text style={styles.iconBtnText}>Details</Text>
{/* Actions Section */}
<View style={styles.actionRow}>
<TouchableOpacity
style={[styles.detailsBtn, { borderColor: colors.border }]}
onPress={() => navigation.navigate('LiveTenderDetail', data)}
>
<AppText variant="bodySmall" style={{ color: colors.textSecondary, fontWeight: '600' }}>
View Details
</AppText>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.mainButton,
purchaseEnded
? styles.btnDisabled
: (purchased ? styles.btnDisabled : isPurchased
? styles.btnPurchased
: styles.btnActive),
]}
onPress={onPurchasePress}
disabled={purchaseEnded || purchased}
onPress={isPurchased ? () => navigation.navigate(NavigationString.PurchaseTenderList) : onPurchasePress}
style={styles.actionBtnWrapper}
>
<Text
<View
style={[
styles.mainButtonText,
isPurchased && !purchaseEnded ? { color: colors.white } : { color: '#fff' },
styles.actionBtn,
{
backgroundColor: (purchaseEnded || purchased)
? colors.textSecondary
: isPurchased
? colors.success // Green for Go to Cart
: colors.primary // Orange for Add
}
]}
>
{purchaseEnded ? (purchased ? 'Purchased' : 'Expired') : (purchased ? 'Purchased' : isPurchased ? 'Remove' : 'Add to Cart')}
</Text>
{(!purchaseEnded && !purchased) && (
<ArrowRight size={16} color={isPurchased ? colors.white : '#fff'} />
)}
<AppText variant="button" style={{ color: '#fff', fontSize: 12 }}>
{purchaseEnded ? (purchased ? 'Purchased' : 'Closed') : (purchased ? 'Purchased' : isPurchased ? 'GO TO CART' : 'Add to Cart')}
</AppText>
{(!purchaseEnded && !purchased) && <ArrowRight size={14} color="#fff" />}
</View>
</TouchableOpacity>
</View>
</TouchableOpacity>
</View>
</View>
</TouchableOpacity>
);
};
export default memo(LiveTenderTile);
const styles = StyleSheet.create({
cardWrapper: {
marginVertical: 10,
marginHorizontal: 16,
backgroundColor: '#fff',
borderRadius: 24,
flexDirection: 'row',
overflow: 'hidden',
// Modern shadow
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.08,
shadowRadius: 20,
elevation: 8,
},
statusTag: {
width: 6,
height: '100%',
},
container: {
flex: 1,
padding: 20,
marginHorizontal: 16,
marginVertical: 10,
borderRadius: 20,
borderWidth: 1,
overflow: 'hidden',
elevation: 4,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
},
header: {
topLine: {
height: 4,
width: '100%',
},
contentContainer: {
padding: 16,
},
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
badge: {
idTag: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primary + '25',
paddingHorizontal: 12,
paddingVertical: 5,
borderRadius: 12,
paddingVertical: 4,
paddingHorizontal: 10,
borderRadius: 20,
gap: 6,
},
badgeText: {
fontSize: 12,
title: {
marginBottom: 16,
fontWeight: '700',
color: colors.primary,
},
priceText: {
fontSize: 20,
fontWeight: '800',
color: '#0f172a',
},
workTitle: {
fontSize: 16,
fontWeight: '600',
color: '#334155',
lineHeight: 24,
},
infoContainer: {
borderRadius: 16,
padding: 12,
marginBottom: 16,
},
infoGrid: {
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: '#f0f4f8',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 16,
marginBottom: 18,
alignItems: 'center',
},
infoItem: {
infoCol: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
flex: 1,
},
infoTextGroup: {
flexDirection: 'column',
divider: {
width: 1,
height: 30,
backgroundColor: 'rgba(0,0,0,0.05)',
marginHorizontal: 10,
},
infoLabel: {
fontSize: 10,
color: '#94a3b8',
textTransform: 'uppercase',
fontWeight: '700',
iconBox: {
// width: 32,
// height: 32,
// borderRadius: 16,
// alignItems: 'center',
// justifyContent: 'center',
// backgroundColor: 'rgba(0,0,0,0.03)',
},
infoValue: {
fontSize: 13,
fontWeight: '700',
color: '#1e293b',
},
actionBar: {
actionRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 12,
},
iconBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 10,
backgroundColor: '#e2e8f0',
},
iconBtnText: {
fontSize: 14,
fontWeight: '700',
color: colors.primary,
},
mainButton: {
detailsBtn: {
flex: 1,
height: 46,
borderRadius: 16,
paddingVertical: 10,
borderWidth: 1,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
actionBtnWrapper: {
flex: 1.5,
},
actionBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
},
btnActive: {
backgroundColor: '#4ade80', // light green
},
btnPurchased: {
backgroundColor: colors.error,
borderWidth: 2,
borderColor: colors.error,
},
btnDisabled: {
backgroundColor: '#e2e8f0',
},
mainButtonText: {
fontSize: 14,
fontWeight: '800',
paddingVertical: 10,
borderRadius: 14,
gap: 6,
},
});
@@ -7,7 +7,6 @@ import {
ScrollView,
SafeAreaView,
Platform,
StatusBar,
} from 'react-native';
import Modal from 'react-native-modal';
import moment from 'moment';
@@ -33,7 +32,7 @@ interface Props {
const TenderFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const inset = useSafeAreaInsets();
const { data: tenderResponse }: any = useFetchTenderById(data?.tenderId);
const tender = tenderResponse?.data?.[0];
@@ -74,8 +73,6 @@ const TenderFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
statusBarTranslucent
>
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" />
{/* Header */}
<View style={[styles.header, { paddingTop: inset.top }]}>
<TouchableOpacity onPress={onClose} style={styles.backBtn}>
+353 -420
View File
@@ -1,12 +1,24 @@
import { StyleSheet, Text, View, ScrollView, StatusBar, TouchableOpacity, RefreshControl, ActivityIndicator, Platform } from 'react-native'
import React, { useCallback, useEffect } from 'react'
import { StyleSheet, View, ScrollView, TouchableOpacity, RefreshControl, ActivityIndicator } from 'react-native'
import React, { useCallback } from 'react'
import { InsideHeader } from '../../Component/Index'
import colors from '../../Constant/Color'
import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'
import { useFetchTenderById, useProceedToBid } from '../../Services/ApiCall/Hooks'
import { useTheme } from '../../Hooks/useTheme'
import AppText from '../../components/AppText'
import { CircleDot, Clock, CheckCircle, XCircle, Calendar, FileText, Shield, AlertCircle } from 'lucide-react-native'
import Animated, { FadeInUp, FadeInDown, FadeIn, SlideInRight, Layout } from 'react-native-reanimated'
import { apiCall } from '../../Axios/Axios'
import NavigationString from '../../Constant/NavigationString'
import DeviceInfo from 'react-native-device-info'
import { BASE_URL } from '../../Services/ApiCall/ApiUrls'
import { useSelector } from 'react-redux'
import { useESetuAdharVarification } from '../../Services/ApiCall/Hooks'
import ConfirmDialog from '../../Constant/Dialog/ConfirmDialog'
import { generateTransactionId } from '../../Helper/uniqueId'
const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView)
const ProceedToBidDetail = () => {
// Sample data based on your JSON
const { colors, spacing, shadows, typography } = useTheme();
const route = useRoute<any>();
const tenderDataId = route.params?.tender;
const { data, refetch, isRefetching } = useProceedToBid(tenderDataId?.tenderId);
@@ -15,101 +27,95 @@ const ProceedToBidDetail = () => {
const [refreshing, setRefreshing] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const navigation = useNavigation<any>()
// Format date from array
const navigation = useNavigation<any>();
const user = useSelector((state: any) => state.token?.token);
const { mutateAsync: eSetuAdharVarification, isPending: isESetuPending } = useESetuAdharVarification();
const [showESetuDialog, setShowESetuDialog] = React.useState(false);
const [isESetuNavigating, setIsESetuNavigating] = React.useState(false);
const handleStartBidding = () => {
const handleStartBidding = async () => {
setLoading(true);
try {
setLoading(true);
navigation.navigate('TenderBidding', { tenderId: tenderDataId.tenderId });
const deviceId = await DeviceInfo.getUniqueId();
const url = `${BASE_URL}tenderBidding/getDsc-with-device-details?deviceId=${deviceId}`;
const response: any = await apiCall('get', url);
console.log("Response: ", response);
const { dsc, deviceBinding, deviceVerification, dscExpiryStatus } = response?.data || {};
if (!dsc) {
navigation.navigate(NavigationString.DscBinding, {
message: "Please map your DSC from the web screen"
});
} else if (!dscExpiryStatus) {
navigation.navigate(NavigationString.DscBinding, {
message: "Your DSC has been expired. Please map new DSC from the web screen"
});
} else if (deviceBinding && deviceVerification === 'PENDING') {
navigation.navigate(NavigationString.DeviceBinding, {
message: "Please approve this device to enable bidding access from the web.",
showBindButton: false
});
} else if (deviceBinding && deviceVerification === 'REVOKE') {
navigation.navigate(NavigationString.DeviceBinding, {
message: "Your device has been revoked. Please bind your device from the web portal.",
showBindButton: true
});
} else if (!deviceBinding) {
navigation.navigate(NavigationString.DeviceBinding, {
message: "Please bind your device from the web portal.",
showBindButton: true
});
} else {
// All checks passed (dsc is true, device is bound and approved)
// Now check Aadhaar verification (eSetu)
// setShowESetuDialog(true);
navigation.navigate(NavigationString.TenderBidding, {
tenderId: tenderDataId.tenderId
});
}
} catch (error) {
console.log("Error in starting bidding: ", error);
} finally {
setLoading(false);
}
}
const handleProceedESetu = async () => {
try {
setIsESetuNavigating(true);
const payload = {
contId: user.id,
transactionId: generateTransactionId(),
udf1: "23.77",
udf2: "77.34",
udf3: "string",
udf4: "string",
responseUrl: "http://dev.192.168.6.194:8090/"
};
const res = await eSetuAdharVarification(payload);
setShowESetuDialog(false);
// Pass returnRoute so KycWebView can come back here
navigation.navigate(NavigationString.KycWebViewScreen, {
res,
returnRoute: NavigationString.TenderBidding,
returnParams: { tenderId: tenderDataId.tenderId }
});
} catch (err) {
console.log('eSetu Error:', err);
setIsESetuNavigating(false);
} finally {
setIsESetuNavigating(false);
}
};
const formatDate = (dateArray: any[]) => {
if (!dateArray || dateArray.length < 3) return "N/A";
const [year, month, day, hour = 0, minute = 0] = dateArray;
return `${day.toString().padStart(2, '0')}/${month.toString().padStart(2, '0')}/${year} ${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
};
// Status indicator component
const StatusIndicator = ({ status, label }: { status: boolean; label: string }) => (
<View style={styles.statusContainer}>
<View style={[styles.statusDot, { backgroundColor: status ? colors.success : colors.error }]} />
<Text style={[styles.statusText, { color: status ? colors.success : colors.error }]}>
{label}: {status ? "Submitted" : "Not Submitted"}
</Text>
</View>
);
// Envelope log card component
const EnvelopeLogCard = ({ log, title }: { log: any; title: string }) => (
<View style={styles.envelopeCard}>
<View style={styles.cardHeader}>
<Text style={styles.cardTitle}>{title}</Text>
<View style={[styles.envelopeBadge, { backgroundColor: colors.primary }]}>
<Text style={styles.envelopeBadgeText}>{log?.envelope}</Text>
</View>
</View>
<View style={styles.logDetailRow}>
<Text style={styles.label}>Activity:</Text>
<Text style={styles.value}>{log?.activity}</Text>
</View>
<View style={styles.logDetailRow}>
<Text style={styles.label}>Date & Time:</Text>
<Text style={styles.value}>{log?.dateTime}</Text>
</View>
<View style={styles.logDetailRow}>
<Text style={styles.label}>System Info:</Text>
<Text style={styles.value}>{log?.osName} / {log?.browserName}</Text>
</View>
<View style={styles.logDetailRow}>
<Text style={styles.label}>IP Address:</Text>
<Text style={styles.value}>{log?.ipAddress}</Text>
</View>
<View style={styles.logDetailRow}>
<Text style={styles.label}>Status Message:</Text>
<Text style={[styles.value, styles.successText]}>{log?.logMessage}</Text>
</View>
{log?.headingMsg && (
<View style={styles.logDetailRow}>
<Text style={styles.label}>Heading:</Text>
<Text style={styles.value}>{log?.headingMsg}</Text>
</View>
)}
</View>
);
// Information row component
const InfoRow = ({ label, value, isHighlighted = false }: { label: string; value: string; isHighlighted?: boolean }) => (
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{label}:</Text>
<Text style={[styles.infoValue, isHighlighted && styles.highlightedValue]}>{value}</Text>
</View>
);
// Feature chip component
const FeatureChip = ({ label, enabled }: { label: string; enabled: boolean }) => (
<View style={[styles.chip, { backgroundColor: enabled ? colors.successLight : colors.lightGray }]}>
<Text style={[styles.chipText, { color: enabled ? colors.success : colors.darkGray }]}>
{label} {enabled ? '✓' : '✗'}
</Text>
</View>
);
const onRefresh = async () => {
try {
setRefreshing(true);
@@ -125,483 +131,410 @@ const ProceedToBidDetail = () => {
}, [tenderData?.tenderId])
);
// Status indicator component with animation
const StatusIndicator = ({ status, label, index }: { status: boolean; label: string; index: number }) => (
<Animated.View
entering={FadeInUp.delay(100 + index * 80).springify()}
style={styles.statusContainer}
>
{status ? (
<CheckCircle size={18} color={colors.success} strokeWidth={2} />
) : (
<XCircle size={18} color={colors.error} strokeWidth={2} />
)}
<AppText variant="bodySmall" color={status ? colors.success : colors.error}>
{label}: {status ? "Submitted" : "Not Submitted"}
</AppText>
</Animated.View>
);
// Envelope log card component with animation
const EnvelopeLogCard = ({ log, title, index }: { log: any; title: string; index: number }) => (
<Animated.View
entering={SlideInRight.delay(100 + index * 100).springify()}
style={[styles.envelopeCard, { borderColor: colors.border }]}
>
<View style={styles.cardHeader}>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>{title}</AppText>
<View style={[styles.envelopeBadge, { backgroundColor: colors.primary }]}>
<AppText variant="caption" color={colors.buttonText} style={{ fontWeight: '700' }}>{log?.envelope}</AppText>
</View>
</View>
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>Activity:</AppText>
<AppText variant="caption" color={colors.text}>{log?.activity}</AppText>
</View>
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>Date & Time:</AppText>
<AppText variant="caption" color={colors.text}>{log?.dateTime}</AppText>
</View>
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>System Info:</AppText>
<AppText variant="caption" color={colors.text}>{log?.osName} / {log?.browserName}</AppText>
</View>
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>IP Address:</AppText>
<AppText variant="caption" color={colors.text}>{log?.ipAddress}</AppText>
</View>
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>Status Message:</AppText>
<AppText variant="caption" color={colors.success}>{log?.logMessage}</AppText>
</View>
{log?.headingMsg && (
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>Heading:</AppText>
<AppText variant="caption" color={colors.text}>{log?.headingMsg}</AppText>
</View>
)}
</Animated.View>
);
// Information row component with animation
const InfoRow = ({ label, value, isHighlighted = false, index }: { label: string; value: string; isHighlighted?: boolean; index: number }) => (
<Animated.View
entering={FadeInUp.delay(50 + index * 50).springify()}
style={[styles.infoRow, { borderBottomColor: colors.border }]}
>
<AppText variant="bodySmall" color={colors.textSecondary}>{label}:</AppText>
<AppText variant="bodySmall" color={isHighlighted ? colors.success : colors.text} style={{ fontWeight: '600' }}>{value}</AppText>
</Animated.View>
);
const styles = makeStyles(colors, spacing, shadows);
return (
<View style={{ flex: 1, backgroundColor: colors.white }}>
<View style={{ flex: 1, backgroundColor: colors.background }}>
<InsideHeader title="Tender Detail" showArrow />
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}
<AnimatedScrollView
style={styles.container}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[colors.primary]} // Android
tintColor={colors.primary} // iOS
colors={[colors.primary]}
tintColor={colors.primary}
/>
}>
}
>
{/* Tender Header Section */}
<View style={styles.headerCard}>
<Animated.View
entering={FadeInDown.duration(400).springify()}
layout={Layout.springify()}
style={[styles.headerCard, shadows.medium]}
>
<View style={styles.tenderHeader}>
<View>
<Text style={styles.tenderNumber}>{tenderData?.tenderNumber}</Text>
<Text style={styles.tenderId}>ID: {tenderData?.tenderId}</Text>
<AppText variant="h3" color={colors.primary} style={{ fontWeight: '700' }}>{tenderData?.tenderNumber}</AppText>
<AppText variant="caption" color={colors.textSecondary}>ID: {tenderData?.tenderId}</AppText>
</View>
<View style={[styles.statusBadge, { backgroundColor: tenderData?.biddingStatus ? colors.success : colors.warning }]}>
<Text style={styles.statusBadgeText}>
<Animated.View
entering={SlideInRight.delay(300).springify()}
style={[styles.statusBadge, { backgroundColor: tenderData?.biddingStatus ? colors.success : colors.primary }]}
>
<AppText variant="caption" color={colors.buttonText} style={{ fontWeight: '600' }}>
{tenderData?.biddingStatus ? "Active" : "Inactive"}
</Text>
</View>
</AppText>
</Animated.View>
</View>
<Text style={styles.workDescription}>{tenderData?.nameofWork}</Text>
<AppText variant="body" color={colors.text} style={{ marginTop: spacing.s, lineHeight: 22, fontSize: typography.size.s, color: colors.textSecondary }}>{tenderData?.nameofWork}</AppText>
<View style={styles.headerInfoRow}>
<View style={[styles.headerInfoRow, { backgroundColor: colors.surface }]}>
<View style={styles.headerInfoItem}>
<Text style={styles.headerInfoLabel}>NIT No.</Text>
<Text style={styles.headerInfoValue}>{tenderData?.nitNo}</Text>
<AppText variant="caption" color={colors.textSecondary}>NIT No.</AppText>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>{tenderData?.nitNo}</AppText>
</View>
<View style={styles.separator} />
<View style={[styles.separator, { backgroundColor: colors.border }]} />
<View style={styles.headerInfoItem}>
<Text style={styles.headerInfoLabel}>EMD Amount</Text>
<Text style={styles.headerInfoValue}>₹{tenderData?.emdInFig}</Text>
<AppText variant="caption" color={colors.textSecondary}>EMD Amount</AppText>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>₹{tenderData?.emdInFig}</AppText>
</View>
</View>
</View>
</Animated.View>
{/* Envelope Submission Status */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Envelope Submission Status</Text>
<View style={styles.statusGrid}>
<StatusIndicator status={tenderData?.envelopeASubmittStatus} label="Envelope A" />
{tenderData?.showEB &&<StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope B" />}
{tenderData?.showLCS &&<StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope LCS" />}
{tenderData?.showQCBS &&<StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope QCBS" />}
{tenderData?.showEC &&<StatusIndicator status={tenderData?.envelopeCSubmittStatus} label="Envelope C" />}
<Animated.View
entering={FadeInUp.delay(200).springify()}
style={styles.section}
>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Envelope Submission Status</AppText>
<View style={[styles.statusGrid, shadows.light]}>
<StatusIndicator status={tenderData?.envelopeASubmittStatus} label="Envelope A" index={0} />
{tenderData?.showEB && <StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope B" index={1} />}
{tenderData?.showLCS && <StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope LCS" index={2} />}
{tenderData?.showQCBS && <StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope QCBS" index={3} />}
{tenderData?.showEC && <StatusIndicator status={tenderData?.envelopeCSubmittStatus} label="Envelope C" index={4} />}
</View>
</View>
</Animated.View>
{/* Envelope Logs */}
{tenderData?.envelopeA_Logs && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Submission Logs</Text>
{tenderData?.envelopeA_Logs && <EnvelopeLogCard log={tenderData?.envelopeA_Logs} title="Envelope A Submission" />}
{tenderData?.envelopeB_Logs && <EnvelopeLogCard log={tenderData?.envelopeB_Logs} title="Envelope B Submission" />}
{tenderData?.envelopeC_Logs && <EnvelopeLogCard log={tenderData?.envelopeC_Logs} title="Envelope C Submission" />}
</View>
<Animated.View
entering={FadeInUp.delay(300).springify()}
style={styles.section}
>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Submission Logs</AppText>
{tenderData?.envelopeA_Logs && <EnvelopeLogCard log={tenderData?.envelopeA_Logs} title="Envelope A Submission" index={0} />}
{tenderData?.envelopeB_Logs && <EnvelopeLogCard log={tenderData?.envelopeB_Logs} title="Envelope B Submission" index={1} />}
{tenderData?.envelopeC_Logs && <EnvelopeLogCard log={tenderData?.envelopeC_Logs} title="Envelope C Submission" index={2} />}
</Animated.View>
)}
{/* Tender Details */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Tender Details</Text>
<View style={styles.detailsCard}>
<InfoRow label="Envelope Type" value={tenderData?.envelopeType} />
<InfoRow label="Tender Stages" value={tenderData?.tenderStages} />
<InfoRow label="Bid Withdrawal" value={tenderData?.bidWithDraw} isHighlighted={tenderData?.bidWithdrawStatus} />
<InfoRow label="Re-bid" value={tenderData?.reBid} />
<InfoRow label="DSC Status" value={tenderData?.dscStatus ? "Required" : "Not Required"} />
<Animated.View
entering={FadeInUp.delay(400).springify()}
style={styles.section}
>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Tender Details</AppText>
<View style={[styles.detailsCard, shadows.light]}>
<InfoRow label="Envelope Type" value={tenderData?.envelopeType} index={0} />
<InfoRow label="Tender Stages" value={tenderData?.tenderStages} index={1} />
<InfoRow label="Bid Withdrawal" value={tenderData?.bidWithDraw} isHighlighted={tenderData?.bidWithdrawStatus} index={2} />
<InfoRow label="Re-bid" value={tenderData?.reBid} index={3} />
<InfoRow label="DSC Status" value={tenderData?.dscStatus ? "Required" : "Not Required"} index={4} />
</View>
</View>
</Animated.View>
{/* Time Period */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Submission Time Period</Text>
<View style={styles.timeCard}>
<View style={styles.timeRow}>
<Text style={styles.timeLabel}>Starts On:</Text>
<Text style={styles.timeValue}>{formatDate(tenderData?.submittedStartDate)}</Text>
</View>
<View style={styles.timeRow}>
<Text style={styles.timeLabel}>Ends On:</Text>
<Text style={styles.timeValue}>{formatDate(tenderData?.submittedEndDate)}</Text>
</View>
<Animated.View
entering={FadeInUp.delay(500).springify()}
style={styles.section}
>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Submission Time Period</AppText>
<View style={[styles.timeCard, shadows.light]}>
<Animated.View
entering={FadeIn.delay(600)}
style={[styles.timeRow, { borderBottomColor: colors.border }]}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Calendar size={16} color={colors.primary} />
<AppText variant="bodySmall" color={colors.textSecondary}>Starts On:</AppText>
</View>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>{formatDate(tenderData?.submittedStartDate)}</AppText>
</Animated.View>
<Animated.View
entering={FadeIn.delay(700)}
style={styles.timeRow}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Calendar size={16} color={colors.error} />
<AppText variant="bodySmall" color={colors.textSecondary}>Ends On:</AppText>
</View>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>{formatDate(tenderData?.submittedEndDate)}</AppText>
</Animated.View>
</View>
</View>
{/* Feature Flags
<View style={styles.section}>
<Text style={styles.sectionTitle}>Available Features</Text>
<View style={styles.chipsContainer}>
<FeatureChip label="View Bid" enabled={tenderData?.viewBid} />
<FeatureChip label="Proceed to Bid" enabled={tenderData?.proceedtoBid} />
<FeatureChip label="Show Envelope B" enabled={tenderData?.showEB} />
<FeatureChip label="Show Envelope C" enabled={tenderData?.showEC} />
<FeatureChip label="QCBS" enabled={tenderData?.showQCBS} />
<FeatureChip label="LCS" enabled={tenderData?.showLCS} />
</View>
</View> */}
</Animated.View>
{/* Final Status */}
<View style={styles.finalStatusCard}>
<Text style={styles.finalStatusTitle}>Overall Status</Text>
<View style={styles.finalStatusRow}>
<Text style={styles.finalStatusLabel}>All Envelopes Submitted:</Text>
<Text style={[styles.finalStatusValue,
{
color: tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus
? colors.success : colors.error
}]}>
{tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus
? "COMPLETED" : "PENDING"}
</Text>
</View>
<View style={styles.finalStatusRow}>
<Text style={styles.finalStatusLabel}>Bidding Status:</Text>
<Text style={[styles.finalStatusValue,
{ color: tenderData?.biddingStatus ? colors.success : colors.error }]}>
<Animated.View
entering={FadeInUp.delay(600).springify()}
style={[styles.finalStatusCard, { borderColor: colors.primary }]}
>
<AppText variant="body" color={colors.primary} style={{ fontWeight: '700', marginBottom: spacing.m, textAlign: 'center' }}>Overall Status</AppText>
<Animated.View
entering={FadeIn.delay(700)}
style={[styles.finalStatusRow, { borderBottomColor: colors.border }]}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Shield size={16} color={colors.textSecondary} />
<AppText variant="bodySmall" color={colors.text}>All Envelopes Submitted:</AppText>
</View>
<AppText variant="bodySmall" color={tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus ? colors.success : colors.error} style={{ fontWeight: '700' }}>
{tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus ? "COMPLETED" : "PENDING"}
</AppText>
</Animated.View>
<Animated.View
entering={FadeIn.delay(800)}
style={styles.finalStatusRow}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<CircleDot size={16} color={colors.textSecondary} />
<AppText variant="bodySmall" color={colors.text}>Bidding Status:</AppText>
</View>
<AppText variant="bodySmall" color={tenderData?.biddingStatus ? colors.success : colors.error} style={{ fontWeight: '700' }}>
{tenderData?.biddingStatus ? "ACTIVE" : "CLOSED"}
</Text>
</View>
</View>
</AppText>
</Animated.View>
</Animated.View>
<View style={styles.footerSpace} />
</ScrollView>
</AnimatedScrollView>
<View style={styles.footer}>
{
tenderData?.proceedtoBid && !tenderData?.viewBid && tenderData?.biddingStatus && tenderData?.bidWithdrawStatus === false ? (
<Animated.View
entering={FadeInDown.duration(300)}
style={[styles.footer, { borderTopColor: colors.border }]}
>
{tenderData?.proceedtoBid && !tenderData?.viewBid && tenderData?.biddingStatus && tenderData?.bidWithdrawStatus === false ? (
<TouchableOpacity style={styles.proceedButton} onPress={handleStartBidding} disabled={isRefetching}>
{loading ? (
<ActivityIndicator size="small" color={colors.buttonText} />
) : (
<AppText variant="button" color={colors.buttonText}>Start Bidding</AppText>
)}
</TouchableOpacity>
) : (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
<AlertCircle size={16} color={colors.textSecondary} />
<AppText variant="bodySmall" color={colors.textSecondary}>One time bidding is completed</AppText>
</View>
)}
</Animated.View>
<TouchableOpacity style={styles.proceedButton} onPress={handleStartBidding} disabled={isRefetching}>
<Text style={styles.proceedButtonText}>{loading ? <ActivityIndicator size={'small'} color={'#ffff'} /> : 'Start Bidding'}</Text>
</TouchableOpacity>
) :
<Text style={styles.footerText}>One time bidding is completed</Text>
}
</View>
<ConfirmDialog
visible={showESetuDialog}
title="Aadhaar Verification Required"
message="To access bidding, please complete your Aadhaar verification."
confirmText={isESetuPending || isESetuNavigating ? "Processing..." : "Proceed"}
cancelText="Cancel"
onConfirm={handleProceedESetu}
onClose={() => !isESetuPending && !isESetuNavigating && setShowESetuDialog(false)}
loading={isESetuPending || isESetuNavigating}
/>
</View>
)
}
export default ProceedToBidDetail
const styles = StyleSheet.create({
const makeStyles = (colors: any, spacing: any, shadows: any) => StyleSheet.create({
container: {
flex: 1,
padding: 16,
marginBottom: 60,
padding: spacing.m,
},
headerCard: {
backgroundColor: colors.primaryLight,
borderRadius: 12,
padding: 20,
marginBottom: 20,
borderLeftWidth: 4,
borderLeftColor: colors.primary,
backgroundColor: colors.surface,
borderRadius: 16,
padding: spacing.m,
marginBottom: spacing.m,
},
tenderHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
tenderNumber: {
fontSize: 22,
fontWeight: 'bold',
color: colors.primary,
},
tenderId: {
fontSize: 14,
color: colors.darkGray,
marginTop: 4,
},
statusBadge: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 20,
},
statusBadgeText: {
color: colors.white,
fontWeight: '600',
fontSize: 12,
},
workDescription: {
fontSize: 16,
color: colors.darkText,
lineHeight: 22,
marginBottom: 16,
},
headerInfoRow: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
padding: 12,
borderRadius: 10,
padding: spacing.s,
marginTop: spacing.m,
},
headerInfoItem: {
flex: 1,
alignItems: 'center',
},
headerInfoLabel: {
fontSize: 12,
color: colors.darkGray,
marginBottom: 4,
},
headerInfoValue: {
fontSize: 16,
fontWeight: '600',
color: colors.primary,
},
separator: {
width: 1,
height: 30,
backgroundColor: colors.lightGray,
},
section: {
marginBottom: 20,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
color: colors.primary,
marginBottom: 12,
paddingLeft: 4,
marginBottom: spacing.m,
},
statusGrid: {
backgroundColor: colors.white,
backgroundColor: colors.surface,
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
padding: spacing.m,
},
statusContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
statusDot: {
width: 12,
height: 12,
borderRadius: 6,
marginRight: 10,
},
statusText: {
fontSize: 15,
fontWeight: '500',
marginBottom: spacing.s,
gap: 8,
},
envelopeCard: {
backgroundColor: colors.white,
backgroundColor: colors.surface,
borderRadius: 12,
padding: 16,
marginBottom: 12,
padding: spacing.m,
marginBottom: spacing.s,
borderWidth: 1,
borderColor: colors.lightGray,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
cardTitle: {
fontSize: 16,
fontWeight: '600',
color: colors.primary,
marginBottom: spacing.s,
},
envelopeBadge: {
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
},
envelopeBadgeText: {
color: colors.white,
fontWeight: 'bold',
fontSize: 12,
},
logDetailRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
label: {
fontSize: 14,
color: colors.darkGray,
flex: 1,
},
value: {
fontSize: 14,
color: colors.darkText,
fontWeight: '500',
flex: 2,
textAlign: 'right',
},
successText: {
color: colors.success,
fontWeight: '600',
marginBottom: 6,
},
detailsCard: {
backgroundColor: colors.white,
backgroundColor: colors.surface,
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
padding: spacing.m,
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: colors.lightestGray,
},
infoLabel: {
fontSize: 14,
color: colors.darkGray,
},
infoValue: {
fontSize: 15,
fontWeight: '500',
color: colors.darkText,
},
highlightedValue: {
color: colors.success,
fontWeight: '600',
paddingVertical: spacing.s,
borderBottomWidth: StyleSheet.hairlineWidth,
},
timeCard: {
backgroundColor: colors.white,
backgroundColor: colors.surface,
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
padding: spacing.m,
},
timeRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: colors.lightestGray,
},
timeLabel: {
fontSize: 15,
color: colors.darkGray,
fontWeight: '500',
},
timeValue: {
fontSize: 15,
color: colors.primary,
fontWeight: '600',
},
chipsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 10,
},
chip: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
marginBottom: 8,
},
chipText: {
fontSize: 14,
fontWeight: '500',
paddingVertical: spacing.s,
borderBottomWidth: StyleSheet.hairlineWidth,
},
finalStatusCard: {
backgroundColor: colors.primaryLight,
backgroundColor: colors.surface,
borderRadius: 12,
padding: 20,
marginTop: 10,
padding: spacing.m,
marginTop: spacing.s,
marginBottom: spacing.l,
borderWidth: 2,
borderColor: colors.primary,
},
finalStatusTitle: {
fontSize: 18,
fontWeight: 'bold',
color: colors.primary,
marginBottom: 16,
textAlign: 'center',
},
finalStatusRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: 'rgba(0, 0, 0, 0.1)',
},
finalStatusLabel: {
fontSize: 16,
color: colors.darkText,
fontWeight: '500',
},
finalStatusValue: {
fontSize: 16,
fontWeight: 'bold',
paddingVertical: spacing.s,
borderBottomWidth: StyleSheet.hairlineWidth,
},
footerSpace: {
height: 30,
},
success: {
color: colors.success,
},
error: {
color: colors.error,
},
primary: {
color: colors.primary,
},
warning: {
color: colors.warning,
},
successLight: {
backgroundColor: colors.successLight,
},
lightGray: {
backgroundColor: colors.lightGray,
height: 80,
},
footer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: colors.white,
padding: 16,
backgroundColor: colors.surface,
padding: spacing.m,
borderTopWidth: 1,
borderTopColor: colors.lightGray,
elevation: 10,
},
proceedButton: {
backgroundColor: colors.primary,
paddingVertical: 14,
borderRadius: 30,
alignItems: 'center',
},
proceedButtonText: {
color: colors.white,
fontSize: 16,
fontWeight: '600',
},
footerText: {
textAlign: 'center',
color: colors.darkGray,
fontSize: 14,
},
actionButton: {
width: '100%',
height: 48,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
}),
},
});
+447 -206
View File
@@ -1,255 +1,496 @@
import React, { useState } from 'react';
import { View, Text, FlatList, StyleSheet, TouchableOpacity, Alert } from 'react-native';
import {
View,
FlatList,
StyleSheet,
TouchableOpacity,
ActivityIndicator,
} from 'react-native';
import Animated, { FadeInDown, FadeIn } from 'react-native-reanimated';
import {
CreditCard,
Calendar,
Hash,
IndianRupee,
Percent,
Server,
Wifi,
WifiOff,
Receipt,
CheckCircle2,
} from 'lucide-react-native';
import { InsideHeader, Loader } from '../../Component/Index';
import colors from '../../Constant/Color';
import { CreditCard, Calendar, User, DollarSign, ArrowRight } from 'lucide-react-native';
import { useDispatch, useSelector } from 'react-redux';
import { useOfflineTenderPurchase } from '../../Services/ApiCall/Hooks';
import { StackActions, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
import { showErrorAlert, showToast } from '../../Constant/ShowDailog';
import { clearPurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const ProceedToPurTender = () => {
const navigation: any = useNavigation();
const Dispatch = useDispatch()
const [loading, setLoading] = useState(false);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender);
const navigation: any = useNavigation();
const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const [loading, setLoading] = useState(false);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender);
const { mutate, isPending } = useOfflineTenderPurchase()
const calculateTotalAmount = (item: any) => {
const gstAmount =
(Number(item.documentFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100);
const { mutate, isPending } = useOfflineTenderPurchase();
const payableAmount =
Number(item.documentFees) + Number(item.portalCharge) + gstAmount;
return payableAmount
}
const calculateTotalAmount = (item: any) => {
const gstAmount =
(Number(item.documentFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100);
return Number(item.documentFees) + Number(item.portalCharge) + gstAmount;
};
const handleOfflinePurchase = async () => {
const orderNo = `ORD${Date.now()}`;
const body = {
tenderDetails: SelectedPurchaseList.map((item: any, index: number) => ({
gst: item.gst,
regId: item.regId || 0,
deptId: item.deptId || 1,
gstAmount: item.gst / 100,
payableAmount: calculateTotalAmount(item),
portalCharge: item.portalCharge,
tenderFees: item.documentFees,
tenderId: item.tenderId,
tenderNumber: item.tenderNumber,
tenderStatus: item.tenderStatus ?? "",
tenderType: item.tenderType ?? "",
isPurchased: item.purchaseStatus ?? false,
totalPayableAmount: calculateTotalAmount(item),
percentageAmount: calculateTotalAmount(item) / 100,
newTenderDocumentFees: item.documentFees * index + 1,
})),
orderNo: orderNo,
};
const grandTotal = SelectedPurchaseList.reduce(
(sum: number, item: any) => sum + calculateTotalAmount(item),
0
);
try {
mutate(body, {
onSuccess: (res) => {
if (res.statusCode === 200) {
showToast(res.message);
navigation.dispatch(StackActions.pop(2));
Dispatch(
clearPurchaseTender()
);
}
},
onError: (error) => {
showErrorAlert('Something went wrong');
console.log(error, "================purchse Error");
}
})
} catch (error) {
console.log("purchase offline error", error);
}
}
const handlePurchase = (type: 'online' | 'offline') => {
setLoading(true);
setTimeout(() => {
setLoading(false);
type === 'online' ? showErrorAlert('Online Purchase is disabled') : handleOfflinePurchase();
}, 1200);
const handleOfflinePurchase = async () => {
const orderNo = `ORD${Date.now()}`;
const body = {
tenderDetails: SelectedPurchaseList.map((item: any, index: number) => ({
gst: item.gst,
regId: item.regId || 0,
deptId: item.deptId || 1,
gstAmount: item.gst / 100,
payableAmount: calculateTotalAmount(item),
portalCharge: item.portalCharge,
tenderFees: item.documentFees,
tenderId: item.tenderId,
tenderNumber: item.tenderNumber,
tenderStatus: item.tenderStatus ?? '',
tenderType: item.tenderType ?? '',
isPurchased: item.purchaseStatus ?? false,
totalPayableAmount: calculateTotalAmount(item),
percentageAmount: calculateTotalAmount(item) / 100,
newTenderDocumentFees: item.documentFees * index + 1,
})),
orderNo,
};
const renderItem = ({ item }: { item: any[0] }) => (
<View style={styles.card}>
<View style={styles.cardHeader}>
<Text style={styles.title}>{item.auctionNumber}</Text>
<Text style={styles.pattern}>{item.auctionPattern}</Text>
</View>
try {
mutate(body, {
onSuccess: (res: any) => {
if (res.statusCode === 200) {
showToast(res.message);
navigation.dispatch(StackActions.pop(2));
Dispatch(clearPurchaseTender());
}
},
onError: (error: any) => {
showErrorAlert('Something went wrong');
console.log(error, '================purchse Error');
},
});
} catch (error) {
console.log('purchase offline error', error);
}
};
<View style={styles.cardRow}>
<Calendar size={18} color={colors.primary} />
<Text style={styles.text}>End Date: {item.purchaseEnd}</Text>
</View>
const handlePurchase = (type: 'online' | 'offline') => {
setLoading(true);
setTimeout(() => {
setLoading(false);
type === 'online'
? showErrorAlert('Online Purchase is disabled')
: handleOfflinePurchase();
}, 1200);
};
<View style={styles.cardRow}>
<User size={18} color={colors.primary} />
<Text style={styles.text}>Tender No: {item.tenderNumber}</Text>
</View>
const styles = makeStyles(colors, spacing, shadows);
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>Tender Fees: ₹{item.documentFees}</Text>
</View>
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>GST ({item.gst}%): ₹{item.gst / 100}</Text>
</View>
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>Portal Charge: ₹{item.portalCharge}</Text>
</View>
<View style={styles.totalRow}>
<CreditCard size={20} color={colors.white} />
<Text style={styles.total}>Total: ₹{calculateTotalAmount(item)}</Text>
</View>
</View>
);
const renderItem = ({ item, index }: { item: any; index: number }) => {
const total = calculateTotalAmount(item);
const gstAmount =
(Number(item.documentFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100);
return (
<View style={{ flex: 1, backgroundColor: colors.white }}>
<InsideHeader title="Tender Purchase" showArrow />
<View style={styles.screen}>
<FlatList
data={SelectedPurchaseList}
renderItem={renderItem}
keyExtractor={(item) => item?.tenderId?.toString()}
contentContainerStyle={styles.container}
/>
</View>
<Animated.View
entering={FadeInDown.delay(index * 90).springify()}
style={styles.card}
>
{/* Card Top Accent */}
<View style={styles.cardAccent} />
<View style={styles.bottomButtonContainer}>
<TouchableOpacity
style={[styles.button, { marginRight: 8 }]}
onPress={() => handlePurchase('online')}
>
<Text style={styles.buttonText}>Purchase Online</Text>
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, { marginLeft: 8 }]}
onPress={() => handlePurchase('offline')}
>
<Text style={styles.buttonText}>Purchase Offline</Text>
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} />
</TouchableOpacity>
<View style={styles.cardBody}>
{/* Header */}
<View style={styles.cardHeader}>
<View style={styles.indexBadge}>
<AppText variant="caption" color={colors.buttonText} style={{ fontWeight: '800' } as any}>
{String(index + 1).padStart(2, '0')}
</AppText>
</View>
{loading && (
<View style={styles.loadingOverlay}>
<Loader />
<View style={{ flex: 1, marginLeft: spacing.s }}>
<AppText variant="h4" color={colors.text} numberOfLines={2}>
{item.auctionOfWork || item.auctionNumber}
</AppText>
{item.auctionPattern ? (
<View style={styles.patternBadge}>
<AppText variant="caption" color={colors.primary} style={{ fontWeight: '600' } as any}>
{item.auctionPattern}
</AppText>
</View>
)}
) : null}
</View>
</View>
{/* Divider */}
<View style={styles.divider} />
{/* Info Rows */}
<InfoRow
icon={<Hash size={15} color={colors.primary} />}
label="Tender No"
value={item.tenderNumber}
colors={colors}
/>
<InfoRow
icon={<Calendar size={15} color={colors.primary} />}
label="Purchase End"
value={item.purchaseEnd || '—'}
colors={colors}
/>
{/* Divider */}
<View style={[styles.divider, { marginVertical: spacing.s }]} />
{/* Fee breakdown */}
<View style={styles.feeSection}>
<AppText variant="caption" color={colors.textSecondary} style={styles.feeHeader as any}>
FEE BREAKDOWN
</AppText>
<FeeRow
label="Document Fees"
value={`₹ ${Number(item.documentFees).toFixed(2)}`}
colors={colors}
/>
<FeeRow
label={`GST (${item.gst}%)`}
value={`₹ ${gstAmount.toFixed(2)}`}
colors={colors}
/>
<FeeRow
label="Portal Charge"
value={`₹ ${Number(item.portalCharge).toFixed(2)}`}
colors={colors}
/>
</View>
{/* Total Banner */}
<View style={styles.totalBanner}>
<View style={styles.totalLeft}>
<Receipt size={18} color={colors.buttonText} />
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '600' } as any}>
Total Payable
</AppText>
</View>
<AppText variant="h4" color={colors.buttonText}>
₹ {total.toFixed(2)}
</AppText>
</View>
</View>
</Animated.View>
);
};
return (
<View style={styles.screen}>
<InsideHeader title="Tender Purchase" showArrow />
{/* Grand Total Strip */}
<Animated.View entering={FadeInDown.springify()} style={styles.grandTotalStrip}>
<View>
<AppText variant="caption" color={colors.textSecondary}>
{SelectedPurchaseList.length} tender{SelectedPurchaseList.length !== 1 ? 's' : ''}
</AppText>
<AppText variant="h3" color={colors.text}>
₹ {grandTotal.toFixed(2)}
</AppText>
</View>
<View style={styles.grandTotalRight}>
<CheckCircle2 size={18} color={colors.success} />
<AppText variant="caption" color={colors.success} style={{ fontWeight: '600' } as any}>
Ready to pay
</AppText>
</View>
</Animated.View>
<FlatList
data={SelectedPurchaseList}
renderItem={renderItem}
keyExtractor={(item) => item?.tenderId?.toString()}
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
/>
{/* Bottom Action Buttons */}
<View style={styles.footer}>
<TouchableOpacity
style={[styles.button, styles.onlineButton]}
activeOpacity={0.85}
onPress={() => handlePurchase('online')}
>
<Wifi size={18} color={colors.primary} />
<AppText variant="button" color={colors.primary} style={styles.onlineBtnText as any}>
Online
</AppText>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.offlineButton]}
activeOpacity={0.85}
onPress={() => handlePurchase('offline')}
>
<WifiOff size={18} color={colors.buttonText} />
<AppText variant="button" color={colors.buttonText} style={styles.offlineBtnText as any}>
Offline
</AppText>
</TouchableOpacity>
</View>
{/* Loading Overlay */}
{(loading || isPending) && (
<Animated.View entering={FadeIn.duration(200)} style={styles.loadingOverlay}>
<View style={styles.loadingCard}>
<ActivityIndicator size="large" color={colors.primary} />
<AppText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: spacing.s } as any}>
Processing purchase…
</AppText>
</View>
</Animated.View>
)}
</View>
);
};
export default ProceedToPurTender;
const styles = StyleSheet.create({
/* ─── Sub-components ─── */
const InfoRow = ({
icon,
label,
value,
colors,
}: {
icon: React.ReactNode;
label: string;
value: string;
colors: any;
}) => (
<View style={infoRowStyles.row}>
<View style={infoRowStyles.iconWrap}>{icon}</View>
<AppText variant="caption" color={colors.textSecondary} style={infoRowStyles.label as any}>
{label}
</AppText>
<AppText variant="bodySmall" color={colors.text} style={infoRowStyles.value as any}>
{value}
</AppText>
</View>
);
const infoRowStyles = StyleSheet.create({
row: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 5,
gap: 6,
},
iconWrap: {
width: 22,
alignItems: 'center',
},
label: {
flex: 1,
fontWeight: '500',
} as any,
value: {
fontWeight: '600',
} as any,
});
const FeeRow = ({
label,
value,
colors,
}: {
label: string;
value: string;
colors: any;
}) => (
<View style={feeRowStyles.row}>
<AppText variant="caption" color={colors.textSecondary}>
{label}
</AppText>
<AppText variant="caption" color={colors.text} style={{ fontWeight: '600' } as any}>
{value}
</AppText>
</View>
);
const feeRowStyles = StyleSheet.create({
row: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 3,
},
});
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
screen: {
flex: 1,
flex: 1,
backgroundColor: colors.background,
},
grandTotalStrip: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: colors.surface,
marginHorizontal: spacing.m,
marginTop: spacing.m,
marginBottom: spacing.s,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
borderRadius: 18,
...shadows.medium,
},
grandTotalRight: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
backgroundColor: `${colors.success}15`,
paddingHorizontal: spacing.s + 2,
paddingVertical: spacing.xs + 2,
borderRadius: 20,
},
container: {
padding: 16,
paddingBottom: 120,
paddingHorizontal: spacing.m,
paddingTop: spacing.s,
paddingBottom: 120,
},
card: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 16,
marginBottom: 16,
elevation: 5,
shadowColor: '#000',
shadowOpacity: 0.12,
shadowOffset: { width: 0, height: 3 },
shadowRadius: 8,
backgroundColor: colors.surface,
borderRadius: 20,
marginBottom: spacing.m,
overflow: 'hidden',
...shadows.medium,
},
cardAccent: {
height: 4,
backgroundColor: colors.primary,
},
cardBody: {
padding: spacing.m,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 12,
flexDirection: 'row',
alignItems: 'flex-start',
marginBottom: spacing.s,
},
title: {
fontSize: 16,
fontWeight: '700',
color: '#0F172A',
indexBadge: {
width: 32,
height: 32,
borderRadius: 10,
backgroundColor: colors.primary,
alignItems: 'center',
justifyContent: 'center',
marginTop: 2,
},
pattern: {
fontSize: 14,
color: colors.primary,
fontWeight: '600',
patternBadge: {
alignSelf: 'flex-start',
backgroundColor: `${colors.primary}15`,
paddingHorizontal: spacing.s,
paddingVertical: 2,
borderRadius: 8,
marginTop: 4,
},
cardRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
marginBottom: 4,
divider: {
height: StyleSheet.hairlineWidth,
backgroundColor: colors.border,
marginVertical: spacing.s,
},
text: {
fontSize: 14,
color: '#555',
feeSection: {
backgroundColor: colors.overlay,
borderRadius: 12,
padding: spacing.s + 2,
marginBottom: spacing.s,
},
totalRow: {
marginTop: 10,
backgroundColor: colors.primary,
borderRadius: 12,
paddingVertical: 10,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: 8,
feeHeader: {
letterSpacing: 1.2,
marginBottom: spacing.xs + 2,
fontWeight: '700',
} as any,
totalBanner: {
backgroundColor: colors.primary,
borderRadius: 14,
paddingVertical: 12,
paddingHorizontal: spacing.m,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
total: {
fontSize: 15,
fontWeight: '700',
color: '#fff',
totalLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
},
bottomButtonContainer: {
position: 'absolute',
bottom: 0,
left: 0,
flexDirection: 'row',
backgroundColor: colors.white,
width: '100%',
paddingVertical: 20,
paddingHorizontal: 10,
elevation: 5
footer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
flexDirection: 'row',
paddingHorizontal: spacing.m,
paddingTop: spacing.s + 4,
paddingBottom: spacing.m + 4,
backgroundColor: colors.surface,
gap: spacing.s,
...shadows.strong,
},
button: {
flex: 1,
backgroundColor: colors.primary,
paddingVertical: 14,
borderRadius: 12,
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 14,
borderRadius: 16,
gap: spacing.s,
},
buttonText: {
color: '#fff',
fontWeight: '700',
fontSize: 14,
onlineButton: {
backgroundColor: colors.surface,
borderWidth: 1.5,
borderColor: colors.primary,
},
offlineButton: {
backgroundColor: colors.buttonPrimary,
...shadows.medium,
},
onlineBtnText: {
letterSpacing: 1.2,
} as any,
offlineBtnText: {
letterSpacing: 1.2,
} as any,
loadingOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.2)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.3)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
},
});
loadingCard: {
backgroundColor: colors.surface,
borderRadius: 20,
padding: spacing.l,
alignItems: 'center',
...shadows.strong,
},
});
+3 -3
View File
@@ -34,7 +34,7 @@ const PurchaseTenderAllList = () => {
return () => clearTimeout(timer);
}
}, [isRefetching]);
const format = 'DD-MM-YYYY HH:mm:ss';
function isTenderActive(item: any): boolean {
@@ -70,10 +70,10 @@ const PurchaseTenderAllList = () => {
</View>
);
}, [isFetchingNextPage]);
return (
<View style={{ flex: 1 }}>
<SearchInput onChange={() => { }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} />
<SearchInput onChange={() => { }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} filter={false} />
{showRefreshBanner && (
<Animated.View
+370 -183
View File
@@ -1,223 +1,410 @@
import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
import Animated, { FadeIn, FadeOut, Layout, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import { ChevronDown, Trash2, Info } from 'lucide-react-native';
import React, { useState } from 'react';
import {
View,
StyleSheet,
ScrollView,
TouchableOpacity,
useWindowDimensions,
} from 'react-native';
import Animated, {
FadeIn,
FadeOut,
FadeInDown,
Layout,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { ChevronDown, ChevronRight, Trash2, ShoppingBag, FileText, Tag } from 'lucide-react-native';
import { InsideHeader } from '../../Component/Index';
import colors from '../../Constant/Color';
import { useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
import { useDispatch, useSelector } from 'react-redux';
import { togglePurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import { useEffect } from 'react';
const PurchaseTenderList = () => {
const Navigation: any = useNavigation()
const Dispatch = useDispatch()
const Navigation: any = useNavigation();
const Dispatch = useDispatch();
const { colors, spacing, shadows, typography } = useTheme();
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender);
const { width } = useWindowDimensions();
const toggleExpand = (index: number) => {
setExpandedIndex(prev => (prev === index ? null : index));
};
useEffect(()=>{
if(SelectedPurchaseList.length === 0){
Navigation.goBack()
}
},[SelectedPurchaseList]
useEffect(() => {
if (SelectedPurchaseList.length === 0) {
Navigation.goBack();
}
}, [SelectedPurchaseList]);
)
const styles = makeStyles(colors, spacing, shadows);
return (
<View style={styles.screen}>
<InsideHeader title="Purchase" showArrow />
<InsideHeader title="Purchase Tenders" showArrow />
<ScrollView contentContainerStyle={styles.container}>
{/* Cards */}
{SelectedPurchaseList.map((item: any, index: any) => {
const expanded = expandedIndex === index;
return (
<Animated.View
key={item.auctionId}
layout={Layout.springify()}
style={styles.card}
>
<TouchableOpacity
activeOpacity={0.9}
onPress={() => toggleExpand(index)}
>
<CardHeader
title={item.auctionOfWork}
expanded={expanded}
/>
{/* Summary Strip */}
<View style={styles.summaryStrip}>
<View style={styles.summaryIcon}>
<ShoppingBag size={16} color={colors.primary} />
</View>
<AppText variant="caption" color={colors.textSecondary}>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' } as any}>
{SelectedPurchaseList.length}
</AppText>
{' '}tender{SelectedPurchaseList.length !== 1 ? 's' : ''} selected for purchase
</AppText>
</View>
<Text style={styles.subTitle}>
Tender No: {item.tenderNumber}
</Text>
<ScrollView
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
>
{SelectedPurchaseList.map((item: any, index: number) => (
<TenderItem
key={item.auctionId || index}
item={item}
index={index}
expanded={expandedIndex === index}
toggleExpand={toggleExpand}
colors={colors}
styles={styles}
/>
))}
<Text style={styles.subTitle}>
Status:{' '}
<Text
style={{
color: item.tenderStatus === 'Opened' ? '#16A34A' : '#dc6f26ff',
fontWeight: '700',
}}
>
{item.tenderStatus === 'Opened' ? 'Open' : 'Approved'}
</Text>
</Text>
</TouchableOpacity>
{/* Expanded Content */}
{expanded && (
<Animated.View
entering={FadeIn.duration(220)}
exiting={FadeOut.duration(180)}
layout={Layout.springify()}
style={styles.details}
>
{/* {renderRow('', item.departmentName)} */}
{/* {renderRow('Pattern', item.auctionPattern)} */}
{renderRow('Fees', `₹ ${item.documentFees}`)}
{renderRow('Department', item.departmentName)}
{renderRow('Purchase End', item.purchaseEnd)}
<TouchableOpacity style={styles.removeButton} onPress={() => { Dispatch(togglePurchaseTender(item)) }}>
<Trash2 size={18} color="#fff" />
<Text style={styles.removeText}>Remove</Text>
</TouchableOpacity>
</Animated.View>
)}
</Animated.View>
);
})}
<View style={{ height: 90 }} />
</ScrollView>
{/* Footer */}
<TouchableOpacity disabled={Boolean(!SelectedPurchaseList.length)} style={[styles.proceedButton, { backgroundColor: Boolean(!SelectedPurchaseList.length) ? colors.lightText : colors.primary }]} onPress={() => { Navigation.navigate(NavigationString.ProceedToPurTender) }}>
<Text style={styles.proceedText}>Proceed to Purchase</Text>
</TouchableOpacity>
{/* Floating Proceed Button */}
<View style={styles.footerContainer}>
<TouchableOpacity
activeOpacity={0.9}
disabled={!SelectedPurchaseList.length}
style={[
styles.proceedButton,
{
backgroundColor: SelectedPurchaseList.length
? colors.buttonPrimary
: colors.secondary,
},
]}
onPress={() => Navigation.navigate(NavigationString.ProceedToPurTender)}
>
<AppText variant="button" color={colors.buttonText} style={styles.proceedText as any}>
Proceed to Purchase
</AppText>
<View style={styles.countBadge}>
<AppText variant="caption" color={colors.buttonPrimary} style={{ fontWeight: '800' } as any}>
{SelectedPurchaseList.length}
</AppText>
</View>
<ChevronRight size={20} color={colors.buttonText} />
</TouchableOpacity>
</View>
</View>
);
};
export default PurchaseTenderList;
const CardHeader = ({ title, expanded }: {
title: string;
expanded: boolean;
}) => {
const rotation = useSharedValue(expanded ? 1 : 0);
rotation.value = withTiming(expanded ? 1 : 0, { duration: 220 });
const arrowStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value * 180}deg` }],
}));
/* ─── Sub-components ─── */
return (
<View style={styles.cardHeader}>
<Text style={styles.title}>{title}</Text>
<Animated.View style={arrowStyle}>
<ChevronDown size={22} color={colors.black} />
</Animated.View>
</View>
);
};
const renderRow = (label: string, value: string) => (
<View style={styles.row}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.value}>{value}</Text>
const DetailRow = ({
label,
value,
colors,
valueColor,
}: {
label: string;
value: string;
colors: any;
valueColor?: string;
}) => (
<View style={detailRowStyles.row}>
<AppText variant="caption" color={colors.textSecondary} style={detailRowStyles.label as any}>
{label}
</AppText>
<AppText variant="caption" color={valueColor || colors.text} style={detailRowStyles.value as any}>
{value}
</AppText>
</View>
);
const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: '#F8FAFC',
},
container: {
padding: 16,
paddingBottom: 110,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 14,
},
heading: {
fontSize: 22,
fontWeight: '800',
color: colors.black,
},
card: {
backgroundColor: '#fff',
borderRadius: 18,
padding: 16,
marginBottom: 14,
borderWidth: 1,
borderColor: '#E5E7EB',
},
cardHeader: {
const detailRowStyles = StyleSheet.create({
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
title: {
fontSize: 16,
fontWeight: '700',
color: '#0F172A',
flex: 1,
marginRight: 12,
},
subTitle: {
fontSize: 14,
color: '#64748B',
marginTop: 6,
},
details: {
marginTop: 16,
paddingTop: 14,
borderTopWidth: 1,
borderTopColor: '#E5E7EB',
},
row: {
marginVertical: 6,
paddingVertical: 8,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0,0,0,0.06)',
},
label: {
fontSize: 13,
color: '#64748B',
},
flex: 1,
fontWeight: '500',
} as any,
value: {
fontSize: 15,
flex: 1,
textAlign: 'right',
fontWeight: '600',
color: '#0F172A',
},
removeButton: {
marginTop: 18,
backgroundColor: colors.primary,
borderRadius: 14,
paddingVertical: 12,
flexDirection: 'row',
justifyContent: 'center',
gap: 8,
},
removeText: {
color: '#fff',
fontWeight: '700',
},
proceedButton: {
position: 'absolute',
bottom: 16,
left: 16,
right: 16,
backgroundColor: colors.primary,
paddingVertical: 16,
borderRadius: 18,
alignItems: 'center',
},
proceedText: {
color: '#fff',
fontSize: 16,
fontWeight: '800',
},
} as any,
});
/* ─── Chevron animated style hook ─── */
function useChevronStyle(expanded: boolean) {
const rotation = useSharedValue(expanded ? 1 : 0);
rotation.value = withTiming(expanded ? 1 : 0, { duration: 220 });
return useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value * 180}deg` }],
}));
}
/* ─── Extracted Tender Item Component ─── */
const TenderItem = ({ item, index, expanded, toggleExpand, colors, styles }: any) => {
const Dispatch = useDispatch();
const isOpen = item.tenderStatus === 'Opened';
const chevronStyle = useChevronStyle(expanded);
console.log(item, "itemmmmmmm");
return (
<Animated.View
entering={FadeInDown.delay(index * 80).springify()}
layout={Layout.springify()}
style={[styles.card, expanded && styles.cardExpanded]}
>
<View style={[styles.accentBar, { backgroundColor: isOpen ? colors.success : colors.primary }]} />
<View style={styles.cardInner}>
<TouchableOpacity
activeOpacity={0.85}
onPress={() => toggleExpand(index)}
>
<View style={styles.badgeRow}>
<View style={styles.tenderBadge}>
<FileText size={11} color={colors.primary} />
<AppText variant="caption" color={colors.primary} style={styles.badgeText as any}>
{item.tenderNumber}
</AppText>
</View>
</View>
<View style={styles.titleRow}>
<AppText variant="h4" style={styles.title as any} numberOfLines={2}>
{item.auctionOfWork}
</AppText>
<Animated.View style={chevronStyle}>
<ChevronDown size={20} color={colors.textSecondary} />
</Animated.View>
</View>
<View style={styles.deptRow}>
<Tag size={12} color={colors.textSecondary} />
<AppText variant="caption" color={colors.textSecondary} style={styles.deptText as any}>
{item.departmentName || 'Department'}
</AppText>
</View>
</TouchableOpacity>
{expanded && (
<Animated.View
entering={FadeIn.duration(220)}
exiting={FadeOut.duration(160)}
layout={Layout.springify()}
style={styles.detailsContainer}
>
<View style={styles.divider} />
<DetailRow
label="Document Fees"
value={`₹ ${item.documentFees}`}
colors={colors}
valueColor={colors.primary}
/>
<DetailRow
label="Department"
value={item.departmentName}
colors={colors}
/>
<DetailRow
label="Purchase End"
value={item.purchaseEnd}
colors={colors}
/>
<TouchableOpacity
style={styles.removeButton}
activeOpacity={0.85}
onPress={() => Dispatch(togglePurchaseTender(item))}
>
<Trash2 size={16} color="#fff" />
<AppText variant="button" color="#fff" style={styles.removeText as any}>
Remove
</AppText>
</TouchableOpacity>
</Animated.View>
)}
</View>
</Animated.View>
);
};
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
screen: {
flex: 1,
backgroundColor: colors.background,
},
summaryStrip: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.surface,
marginHorizontal: spacing.m,
marginTop: spacing.m,
marginBottom: spacing.s,
paddingHorizontal: spacing.m,
paddingVertical: spacing.s + 2,
borderRadius: 14,
gap: spacing.s,
...shadows.light,
},
summaryIcon: {
width: 28,
height: 28,
borderRadius: 8,
backgroundColor: `${colors.primary}15`,
alignItems: 'center',
justifyContent: 'center',
},
container: {
paddingHorizontal: spacing.m,
paddingTop: spacing.s,
},
card: {
backgroundColor: colors.surface,
borderRadius: 20,
marginBottom: spacing.m,
flexDirection: 'row',
overflow: 'hidden',
...shadows.medium,
},
cardExpanded: {
...shadows.strong,
},
accentBar: {
width: 5,
borderTopLeftRadius: 20,
borderBottomLeftRadius: 20,
},
cardInner: {
flex: 1,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
},
badgeRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: spacing.s,
},
tenderBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: `${colors.primary}15`,
paddingHorizontal: spacing.s,
paddingVertical: 3,
borderRadius: 8,
gap: 4,
},
badgeText: {
fontWeight: '600',
letterSpacing: 0.2,
} as any,
statusBadge: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.s + 2,
paddingVertical: 3,
borderRadius: 20,
gap: 5,
},
statusDot: {
width: 6,
height: 6,
borderRadius: 3,
},
titleRow: {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
gap: spacing.s,
},
title: {
flex: 1,
color: undefined, // will use AppText default from theme
fontWeight: '700',
} as any,
deptRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
marginTop: spacing.xs + 2,
},
deptText: {
letterSpacing: 0.3,
} as any,
detailsContainer: {
marginTop: spacing.s,
},
divider: {
height: 1,
backgroundColor: colors.border,
marginBottom: spacing.s,
},
removeButton: {
marginTop: spacing.m,
backgroundColor: colors.error,
borderRadius: 14,
paddingVertical: 12,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: spacing.s,
},
removeText: {
letterSpacing: 1,
} as any,
footerContainer: {
position: 'absolute',
bottom: 20,
left: spacing.m,
right: spacing.m,
},
proceedButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 16,
borderRadius: 20,
gap: spacing.s,
...shadows.strong,
},
proceedText: {
letterSpacing: 1.5,
} as any,
countBadge: {
backgroundColor: '#fff',
borderRadius: 12,
minWidth: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 6,
},
});
+172 -177
View File
@@ -1,268 +1,263 @@
import React, { use, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Platform } from 'react-native';
import React, { useMemo } from 'react';
import { View, StyleSheet, TouchableOpacity, Platform } from 'react-native';
import Animated, { FadeInUp, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import moment from 'moment';
import CountdownTimer from '../../Component/Counter/CountdownTimer';
import colors from '../../Constant/Color';
import { useFetchTenderById } from '../../Services/ApiCall/Hooks';
import { useNavigation } from '@react-navigation/native';
import TenderFullDetail from './Modal/TenderFullDetail';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
import { Calendar, Wallet, Banknote, ArrowRight, CircleDot, Clock } from 'lucide-react-native';
const PurchaseTenderTile = ({ item, onPressDetails }: any) => {
const scale = useSharedValue(1);
const elevation = useSharedValue(8);
const { colors, spacing, shadows } = useTheme();
const navigation = useNavigation<any>();
const scale = useSharedValue(1);
const elevation = useSharedValue(2); // Reduced initial elevation for a sleeker look
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
elevation: elevation.value,
}));
const { data: tenderResponse, }: any = useFetchTenderById(item?.tenderId);
const { data: tenderResponse }: any = useFetchTenderById(item?.tenderId); // Assuming id is reachable here, verified in original
// Fix: Original code used item?.tenderId, but useFetchTenderById might need checks.
// Keeping logic same as original: item contains tenderId?
// Original check: const { data: tenderResponse, }: any = useFetchTenderById(item?.tenderId);
const tender = tenderResponse?.data;
const handlePressIn = () => {
scale.value = withTiming(0.97, { duration: 120 });
elevation.value = withTiming(3, { duration: 120 });
scale.value = withTiming(0.98, { duration: 150 });
};
const handlePressOut = () => {
scale.value = withTiming(1, { duration: 160 });
elevation.value = withTiming(8, { duration: 160 });
scale.value = withTiming(1, { duration: 150 });
};
const isLive = item?.biddingStatus === true;
const statusColor = isLive ? colors.primary : '#94A3B8';
const lightStatusBg = isLive ? '#EFF6FF' : '#F8FAFC';
const [showDetail, setShowDetail] = React.useState(false);
// Dynamic colors based on status
const statusConfig = useMemo(() => {
if (isLive) {
return {
color: colors.primary,
bg: colors.primary + '15', // 15 = ~8% opacity
label: 'LIVE NOW',
icon: CircleDot
};
}
return {
color: colors.textSecondary,
bg: colors.secondary + '15',
label: 'CLOSED',
icon: Clock
};
}, [isLive, colors]);
const styles = useMemo(() => styling(colors, spacing), [colors, spacing]);
return (
<Animated.View
entering={FadeInUp.duration(400)}
style={[styles.shadowContainer, animatedStyle]}
entering={FadeInUp.duration(500).springify()}
style={[styles.container, shadows.light, animatedStyle]}
>
<TouchableOpacity
activeOpacity={1}
onPress={onPressDetails}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
style={styles.cardInternal}
style={styles.card}
>
<View style={[styles.statusAccent, { backgroundColor: statusColor }]} />
<View style={styles.mainInfo}>
{/* Header */}
<View style={styles.headerRow}>
<View>
<View style={[styles.statusBadge, { backgroundColor: lightStatusBg }]}>
<View style={[styles.statusDot, { backgroundColor: statusColor }]} />
<Text style={[styles.statusText, { color: statusColor }]}>
{isLive ? 'LIVE' : 'CLOSED'}
</Text>
</View>
<Text style={styles.title}>{item?.tenderNumber}</Text>
{/* Header Section */}
<View style={styles.header}>
<View style={styles.headerTopUser}>
<View style={[styles.statusBadge, { backgroundColor: statusConfig.bg }]}>
<statusConfig.icon size={12} color={statusConfig.color} strokeWidth={3} />
<AppText variant="caption" style={[styles.statusText, { color: statusConfig.color }]}>
{statusConfig.label}
</AppText>
</View>
<AppText variant="caption" color={colors.textSecondary} style={{ letterSpacing: 0.5 }}>
ID: {item?.tenderNumber}
</AppText>
</View>
<View style={styles.amountContainer}>
<Text style={styles.amount}>
<View style={{ marginTop: 8 }}>
<AppText variant="bodySmall" color={colors.text} numberOfLines={2}>
{tender?.workDescription || item?.tenderNumber || 'Tender Details'}
</AppText>
</View>
</View>
<View style={styles.divider} />
{/* Info Grid */}
<View style={styles.infoContainer}>
<View style={styles.infoRow}>
<View style={styles.iconBox}>
<Banknote size={18} color={colors.primary} />
</View>
<View>
<AppText variant="caption" color={colors.textSecondary}>Tender Fee</AppText>
<AppText variant="caption" style={{ fontWeight: '700' }}>
₹{Number(item?.tenderFeeInFig || 0).toLocaleString('en-IN')}
</Text>
<Text style={styles.feeLabel}>Tender Fee</Text>
</AppText>
</View>
</View>
{/* Date */}
<TouchableOpacity style={styles.dateRow} onPress={() => {
setShowDetail(true)
}}>
<Text style={styles.dateText}>
<Text style={styles.dateLabel}>Bid Ends: </Text>
{moment(
item?.keydates?.bidSubmissionDate?.endDateTime,
'DD-MM-YYYY HH:mm:ss'
).format('DD MMM YYYY, hh:mm A')}
</Text>
</TouchableOpacity>
{/* Footer */}
<View style={styles.footer}>
<View style={styles.footerTop}>
<Text style={styles.footerLabel}>Remaining Time</Text>
<View style={styles.infoRow}>
<View style={styles.iconBox}>
<Calendar size={18} color={colors.primary} />
</View>
<View>
<AppText variant="caption" color={colors.textSecondary}>Bid Ends</AppText>
<AppText variant="caption" style={{ fontWeight: '600' }}>
{moment(item?.keydates?.bidSubmissionDate?.endDateTime, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, hh:mm A')}
</AppText>
</View>
</View>
</View>
{/* Footer / Timer Action */}
<View style={styles.footer}>
<AppText variant="caption" color={colors.textSecondary} style={{ marginBottom: 4, alignSelf: 'flex-start' }}>
Time Remaining
</AppText>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
{/* Timer */}
<View style={styles.timerContainer}>
{isLive ? (
<CountdownTimer
endDate={
item?.keydates?.bidSubmissionDate?.endDateTime
}
endDate={item?.keydates?.bidSubmissionDate?.endDateTime}
/>
) : (
<Text style={styles.endedText}>--:--:--</Text>
<AppText variant="bodySmall" style={{ fontWeight: '700', color: colors.textSecondary }}>
-- : -- : --
</AppText>
)}
</View>
{/* CTA Button */}
<TouchableOpacity
disabled={!isLive}
onPress={() => {
if (isLive) {
if (isLive && tender) {
navigation.navigate('ProceedToBidDetail', { tender });
} else if (isLive && !tender) {
console.warn("Tender data possibly missing for navigation");
navigation.navigate('ProceedToBidDetail', { tender: item });
}
}}
activeOpacity={0.85}
style={[
styles.actionButton,
{ backgroundColor: statusColor },
!isLive && styles.disabledButton,
{ backgroundColor: isLive ? colors.primary : colors.border },
!isLive && styles.disabledButton
]}
>
<Text style={styles.addText}>
{isLive ? 'Proceed to Bid' : 'Tender Closed'}
</Text>
<AppText variant="button" color={isLive ? colors.buttonText : colors.textSecondary} style={{ marginRight: 8 }}>
{isLive ? 'Bid Now' : 'Closed'}
</AppText>
{isLive && <ArrowRight size={16} color={colors.buttonText} />}
</TouchableOpacity>
</View>
</View>
{/* Tender Detail Modal */}
<TenderFullDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} />
</TouchableOpacity>
</Animated.View>
);
};
export default PurchaseTenderTile;
const styles = StyleSheet.create({
shadowContainer: {
marginHorizontal: 5,
marginTop: 14,
marginBottom: 6,
const styling = (colors: any, spacing: any) => StyleSheet.create({
container: {
marginHorizontal: spacing.s,
marginBottom: spacing.m,
borderRadius: 16,
backgroundColor: '#fff',
elevation: 8,
backgroundColor: colors.surface,
},
cardInternal: {
card: {
borderRadius: 16,
overflow: 'hidden',
flexDirection: 'row',
backgroundColor: '#fff',
backgroundColor: colors.surface,
padding: spacing.m,
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.03)',
},
statusAccent: {
width: 5,
height: '100%',
header: {
marginBottom: spacing.m,
},
mainInfo: {
flex: 1,
padding: 16,
},
headerRow: {
headerTopUser: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 12,
alignItems: 'center',
},
statusBadge: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 6,
marginBottom: 6,
alignSelf: 'flex-start',
},
statusDot: {
width: 6,
height: 6,
borderRadius: 3,
marginRight: 5,
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
gap: 6
},
statusText: {
fontWeight: '700',
fontSize: 10,
fontWeight: '800',
letterSpacing: 0.5,
},
title: {
fontWeight: '800',
fontSize: 16,
color: '#1E293B',
},
amountContainer: {
alignItems: 'flex-end',
},
amount: {
fontWeight: '900',
fontSize: 18,
color: '#0F172A',
},
feeLabel: {
fontSize: 10,
color: '#64748B',
fontWeight: '500',
},
dateRow: {
backgroundColor: '#F8FAFC',
paddingHorizontal: 10,
paddingVertical: 8,
borderRadius: 10,
marginBottom: 16,
borderWidth: 1,
borderColor: '#F1F5F9',
},
dateText: {
fontSize: 12,
color: '#334155',
fontWeight: '600',
},
dateLabel: {
fontWeight: '400',
color: '#64748B',
},
footer: {
borderTopWidth: 1,
borderTopColor: '#F1F5F9',
paddingTop: 12,
},
footerTop: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
footerLabel: {
fontSize: 11,
fontWeight: '600',
color: '#94A3B8',
textTransform: 'uppercase',
},
endedText: {
fontSize: 13,
fontWeight: '700',
color: '#CBD5E1',
divider: {
height: 1,
backgroundColor: colors.border,
marginBottom: spacing.m,
opacity: 0.5
},
actionButton: {
width: '100%',
height: 48,
borderRadius: 12,
infoContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: spacing.l,
gap: spacing.s
},
infoRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
flex: 1,
},
iconBox: {
width: 36,
height: 36,
borderRadius: 10,
backgroundColor: colors.primary + '15', // light opacity primary
justifyContent: 'center',
alignItems: 'center',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
}),
},
footer: {
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: colors.background, // Slightly distinct bg for footer
marginHorizontal: -spacing.m,
marginBottom: -spacing.m,
padding: spacing.m,
borderBottomLeftRadius: 16,
borderBottomRightRadius: 16,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.border,
},
timerContainer: {
flex: 1,
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 12,
},
disabledButton: {
backgroundColor: '#E2E8F0',
},
addText: {
color: '#fff',
fontSize: 14,
fontWeight: '800',
letterSpacing: 0.2,
},
opacity: 0.7
}
});
export default PurchaseTenderTile;
+281 -240
View File
@@ -1,7 +1,6 @@
import React, { useState, useRef, useMemo } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Animated,
@@ -12,40 +11,39 @@ import {
useFetchLiveTender,
useFetchPurchaseTender,
} from '../../Services/ApiCall/Hooks';
import colors from '../../Constant/Color';
import { ShoppingCart, Lock } from 'lucide-react-native';
import { ShoppingCart, Lock, Radio, Package } from 'lucide-react-native';
import { useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
import FontsFamily from '../../Constant/FontsFamily';
import { useSelector } from 'react-redux';
import LiveTender from './LiveTender';
import PurchaseTenderAllList from './PurchaseTenderAllList';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const TAB_HEIGHT = 44;
const TAB_HEIGHT = 48;
const HORIZONTAL_PADDING = 32;
const SWITCH_INSETS = 2;
const SWITCH_INSETS = 3;
const Tender = () => {
const { colors, spacing, shadows } = useTheme();
const Navigation = useNavigation() as any;
const { data: liveTenderData }: any = useFetchLiveTender();
const { data: purchaseTenderData }: any = useFetchPurchaseTender();
const SelectedPurchaseList = useSelector(
(state: any) => state?.selectPurchaseTender,
);
const SelectedPurchaseList = useSelector((state: any) => state?.selectPurchaseTender);
const reduxToken = useSelector((state: any) => state.token?.token);
const liveCount = liveTenderData?.pages?.[0]?.totalCount || 0;
const purchaseCount = purchaseTenderData?.pages?.[0]?.totalCount || 0;
const [selectedTab, setSelectedTab] = useState<'live' | 'purchase'>('live');
const translateX = useRef(new Animated.Value(0)).current;
const TAB_WIDTH = useMemo(() => {
const availableWidth = SCREEN_WIDTH - HORIZONTAL_PADDING - SWITCH_INSETS * 2;
return availableWidth / 2.4;
return availableWidth / 2;
}, []);
const [selectedTab, setSelectedTab] = useState<'live' | 'purchase'>('purchase');
const translateX = useRef(new Animated.Value(TAB_WIDTH)).current;
const onTabPress = (tab: 'live' | 'purchase', index: number) => {
setSelectedTab(tab);
Animated.spring(translateX, {
@@ -55,252 +53,295 @@ const Tender = () => {
}).start();
};
// Check if user is auction type
const isAuctionUser = reduxToken?.userType?.toLowerCase() === 'auction';
const cartCount = SelectedPurchaseList.length;
// Render restricted access message
const renderRestrictedAccess = () => (
<View style={styles.restrictedContainer}>
<View style={styles.restrictedCard}>
<View style={styles.iconContainer}>
<Lock size={64} color={colors.primary} />
const styles = makeStyles(colors, spacing, shadows, TAB_WIDTH, TAB_HEIGHT);
if (isAuctionUser) {
return (
<View style={styles.container}>
<InsideHeader title="Tender" showArrow />
<View style={styles.restrictedContainer}>
<View style={styles.restrictedCard}>
{/* Double-ring Lock Icon */}
<View style={styles.lockRing}>
<View style={styles.lockInner}>
<Lock size={40} color={colors.primary} strokeWidth={1.8} />
</View>
</View>
<AppText variant="h2" centered color={colors.text} style={styles.restrictedTitle as any}>
Access Restricted
</AppText>
<AppText variant="body" centered color={colors.textSecondary} style={styles.restrictedMessage as any}>
You're logged in as an{' '}
<AppText variant="body" color={colors.primary} style={{ fontWeight: '700' } as any}>
Auction user
</AppText>
{'. '}Tender features are not available for your account type.
</AppText>
<View style={styles.hintBox}>
<AppText variant="bodySmall" centered color={colors.textSecondary}>
Switch to a Tender account to access live & purchase tender features.
</AppText>
</View>
<TouchableOpacity
style={styles.loginButton}
activeOpacity={0.85}
onPress={() => Navigation.navigate(NavigationString.Login)}
>
<AppText variant="button" color={colors.buttonText} style={styles.loginBtnText as any}>
Switch to Tender Login
</AppText>
</TouchableOpacity>
</View>
</View>
<Text style={styles.restrictedTitle}>Tender Access Restricted</Text>
<Text style={styles.restrictedMessage}>
You are currently logged in as an Auction user. Tender features are not available for your account type.
</Text>
<Text style={styles.restrictedSubMessage}>
To access tender features, please login with a Tender user account.
</Text>
<TouchableOpacity
style={styles.loginButton}
onPress={() => Navigation.navigate(NavigationString.Login)}
>
<Text style={styles.loginButtonText}>Switch to Tender Login</Text>
</TouchableOpacity>
</View>
</View>
);
);
}
return (
<View style={styles.container}>
<InsideHeader title="Tender" showArrow />
{isAuctionUser ? (
// Show restricted access message for auction users
renderRestrictedAccess()
) : (
// Show normal tender UI for tender users
<>
<View style={styles.topRow}>
<View
{/* Tab Row */}
<View style={styles.topRow}>
{/* Segmented Control */}
<View
style={[
styles.switchWrapper,
{ width: TAB_WIDTH * 2 + SWITCH_INSETS * 2 },
]}
>
{/* Sliding pill */}
<Animated.View
pointerEvents="none"
style={[
styles.activeTab,
{ width: TAB_WIDTH, transform: [{ translateX }] },
]}
/>
{/* Live Tab */}
<TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8}
onPress={() => onTabPress('live', 0)}
>
<Radio
size={13}
color={selectedTab === 'live' ? colors.primary : colors.textSecondary}
strokeWidth={2}
/>
<AppText
variant="caption"
color={selectedTab === 'live' ? colors.primary : colors.textSecondary}
style={[
styles.switchWrapper,
{
width: TAB_WIDTH * 2 + SWITCH_INSETS * 2,
},
styles.tabText,
selectedTab === 'live' && (styles.activeTabText as any),
]}
>
<Animated.View
pointerEvents="none"
style={[
styles.activeTab,
{
width: TAB_WIDTH,
transform: [{ translateX }],
},
]}
/>
Live {liveCount ? `(${liveCount})` : ''}
</AppText>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8}
onPress={() => onTabPress('live', 0)}
>
<Text
style={[
styles.tabText,
selectedTab === 'live' && styles.activeText,
]}
>
Live ({liveCount})
</Text>
</TouchableOpacity>
{/* Purchase Tab */}
<TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8}
onPress={() => onTabPress('purchase', 1)}
>
<Package
size={13}
color={selectedTab === 'purchase' ? colors.primary : colors.textSecondary}
strokeWidth={2}
/>
<AppText
variant="caption"
color={selectedTab === 'purchase' ? colors.primary : colors.textSecondary}
style={[
styles.tabText,
selectedTab === 'purchase' && (styles.activeTabText as any),
]}
>
Purchased {purchaseCount ? `(${purchaseCount})` : ''}
</AppText>
</TouchableOpacity>
</View>
</View>
<TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8}
onPress={() => onTabPress('purchase', 1)}
>
<Text
style={[
styles.tabText,
selectedTab === 'purchase' && styles.activeText,
]}
>
Purchased ({purchaseCount})
</Text>
</TouchableOpacity>
</View>
{/* Content */}
{selectedTab === 'live' ? <LiveTender /> : <PurchaseTenderAllList />}
<View>
{SelectedPurchaseList.length ? (
<View
style={{
borderWidth: 2,
borderColor: colors.white,
backgroundColor: colors.primaryLight,
position: 'absolute',
borderRadius: 50,
zIndex: 20,
right: 0,
top: -10,
paddingHorizontal: 2,
}}
>
<Text style={{ fontSize: 12, color: colors.black }}>
{SelectedPurchaseList.length}
</Text>
</View>
) : null}
<TouchableOpacity
style={{
padding: 10,
backgroundColor: colors.primaryLight,
borderRadius: 20,
}}
onPress={() => {
Navigation.navigate(NavigationString.PurchaseTenderList);
}}
>
<ShoppingCart size={22} color={colors.black} strokeWidth={2} />
</TouchableOpacity>
</View>
{/* Cart Floating Button */}
<TouchableOpacity
style={styles.cartFloatingButton}
activeOpacity={0.9}
onPress={() => Navigation.navigate(NavigationString.PurchaseTenderList)}
>
<ShoppingCart size={24} color={colors.buttonText} strokeWidth={2} />
{cartCount > 0 && (
<View style={styles.cartBadge}>
<AppText variant="caption" color={colors.primary} style={styles.cartBadgeText as any}>
{cartCount}
</AppText>
</View>
{selectedTab === 'live' ? <LiveTender /> : <PurchaseTenderAllList />}
</>
)}
)}
</TouchableOpacity>
</View>
);
};
export default Tender;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.white,
},
topRow: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
paddingHorizontal: 16,
},
switchWrapper: {
flexDirection: 'row',
marginVertical: 14,
backgroundColor: '#f1f5f9',
borderRadius: 18,
padding: 2,
height: TAB_HEIGHT,
},
tab: {
height: TAB_HEIGHT - 4,
alignItems: 'center',
justifyContent: 'center',
zIndex: 2,
},
tabText: {
fontSize: 14,
color: '#64748b',
lineHeight: 18,
fontFamily: FontsFamily.poppinsSemiBold,
},
activeText: {
color: colors.primary,
},
activeTab: {
position: 'absolute',
top: 2,
left: 2,
height: TAB_HEIGHT - 4,
backgroundColor: '#fff',
borderRadius: 16,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowRadius: 6,
elevation: 3,
},
restrictedContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 24,
backgroundColor: colors.lightGray,
},
restrictedCard: {
backgroundColor: colors.white,
borderRadius: 24,
padding: 32,
alignItems: 'center',
maxWidth: 400,
width: '100%',
},
iconContainer: {
width: 120,
height: 120,
borderRadius: 60,
backgroundColor: colors.primaryHighlight,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 24,
},
restrictedTitle: {
fontSize: 24,
fontWeight: '700',
color: colors.darkText,
textAlign: 'center',
marginBottom: 16,
fontFamily: FontsFamily.poppinsBold,
},
restrictedMessage: {
fontSize: 16,
color: colors.grayText,
textAlign: 'center',
lineHeight: 24,
marginBottom: 12,
fontFamily: FontsFamily.poppinsRegular,
},
restrictedSubMessage: {
fontSize: 14,
color: colors.grayText,
textAlign: 'center',
lineHeight: 22,
marginBottom: 32,
fontFamily: FontsFamily.poppinsRegular,
},
loginButton: {
backgroundColor: colors.primary,
paddingHorizontal: 32,
paddingVertical: 14,
borderRadius: 12,
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
},
loginButtonText: {
color: colors.white,
fontSize: 16,
fontWeight: '600',
fontFamily: FontsFamily.poppinsSemiBold,
},
});
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any, TAB_WIDTH: number, TAB_HEIGHT: number) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
/* ── Tab Row ── */
topRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: spacing.m,
paddingVertical: spacing.s + 2,
},
switchWrapper: {
flexDirection: 'row',
backgroundColor: colors.surface,
borderRadius: 16,
padding: SWITCH_INSETS,
height: TAB_HEIGHT,
...shadows.light,
},
tab: {
height: TAB_HEIGHT - SWITCH_INSETS * 2,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 5,
zIndex: 2,
},
tabText: {
fontWeight: '600',
letterSpacing: 0.3,
} as any,
activeTabText: {
fontWeight: '700',
} as any,
activeTab: {
position: 'absolute',
top: SWITCH_INSETS,
left: SWITCH_INSETS,
height: TAB_HEIGHT - SWITCH_INSETS * 2,
backgroundColor: colors.background,
borderRadius: 13,
...shadows.medium,
},
/* ── Cart Floating Button ── */
cartFloatingButton: {
position: 'absolute',
bottom: spacing.xl + 80,
right: spacing.l,
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: colors.primary,
alignItems: 'center',
justifyContent: 'center',
...shadows.strong,
zIndex: 999,
},
cartBadge: {
position: 'absolute',
top: 0,
right: 0,
minWidth: 20,
height: 20,
borderRadius: 10,
backgroundColor: colors.background,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 4,
borderWidth: 1.5,
borderColor: colors.primary,
},
cartBadgeText: {
fontSize: 10,
fontWeight: '800',
lineHeight: 13,
} as any,
/* ── Restricted Access ── */
restrictedContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.l,
backgroundColor: colors.background,
},
restrictedCard: {
backgroundColor: colors.surface,
borderRadius: 28,
padding: spacing.l,
alignItems: 'center',
width: '100%',
maxWidth: 400,
...shadows.strong,
},
lockRing: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: `${colors.primary}12`,
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.l,
borderWidth: 1.5,
borderColor: `${colors.primary}25`,
},
lockInner: {
width: 72,
height: 72,
borderRadius: 36,
backgroundColor: `${colors.primary}18`,
alignItems: 'center',
justifyContent: 'center',
},
restrictedTitle: {
marginBottom: spacing.s + 4,
} as any,
restrictedMessage: {
marginBottom: spacing.s,
lineHeight: 24,
} as any,
hintBox: {
backgroundColor: colors.overlay,
borderRadius: 14,
paddingHorizontal: spacing.m,
paddingVertical: spacing.s + 2,
marginBottom: spacing.l,
width: '100%',
},
loginButton: {
backgroundColor: colors.buttonPrimary,
paddingHorizontal: spacing.l,
paddingVertical: 14,
borderRadius: 16,
width: '100%',
alignItems: 'center',
...shadows.medium,
},
loginBtnText: {
letterSpacing: 1.2,
} as any,
});
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -22,6 +22,8 @@ import PurchaseTenderList from "./Tender/PurchaseTenderList";
import Tender from "./Tender/Tender";
import TenderBidding from "./Tender/tenderBidding/TenderBidding";
import DecryptionScreen from "./TestScreen/DecryptionScreen";
import DeviceBinding from "./Tender/DeviceBinding";
import DscBinding from "./Tender/DscBinding";
const Screens = {
@@ -48,8 +50,8 @@ const Screens = {
ResetPassword,
KycWebViewScreen,
StageThreeScreen,
DecryptionScreen
DecryptionScreen,
DeviceBinding,
DscBinding
}
export default Screens