Files
ProcureLinc-Mobile-App/src/Screens/Tender/ProceedToPurTender.tsx
T

497 lines
14 KiB
TypeScript

import React, { useState } from 'react';
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 { useDispatch, useSelector } from 'react-redux';
import { useOfflineTenderPurchase } from '../../Services/ApiCall/Hooks';
import { StackActions, useNavigation } from '@react-navigation/native';
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 { 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);
return Number(item.documentFees) + Number(item.portalCharge) + gstAmount;
};
const grandTotal = SelectedPurchaseList.reduce(
(sum: number, item: any) => sum + calculateTotalAmount(item),
0
);
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,
};
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);
}
};
const handlePurchase = (type: 'online' | 'offline') => {
setLoading(true);
setTimeout(() => {
setLoading(false);
type === 'online'
? showErrorAlert('Online Purchase is disabled')
: handleOfflinePurchase();
}, 1200);
};
const styles = makeStyles(colors, spacing, shadows);
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 (
<Animated.View
entering={FadeInDown.delay(index * 90).springify()}
style={styles.card}
>
{/* Card Top Accent */}
<View style={styles.cardAccent} />
<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>
<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;
/* ─── 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,
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: {
paddingHorizontal: spacing.m,
paddingTop: spacing.s,
paddingBottom: 120,
},
card: {
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',
alignItems: 'flex-start',
marginBottom: spacing.s,
},
indexBadge: {
width: 32,
height: 32,
borderRadius: 10,
backgroundColor: colors.primary,
alignItems: 'center',
justifyContent: 'center',
marginTop: 2,
},
patternBadge: {
alignSelf: 'flex-start',
backgroundColor: `${colors.primary}15`,
paddingHorizontal: spacing.s,
paddingVertical: 2,
borderRadius: 8,
marginTop: 4,
},
divider: {
height: StyleSheet.hairlineWidth,
backgroundColor: colors.border,
marginVertical: spacing.s,
},
feeSection: {
backgroundColor: colors.overlay,
borderRadius: 12,
padding: spacing.s + 2,
marginBottom: spacing.s,
},
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',
},
totalLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
},
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,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 14,
borderRadius: 16,
gap: spacing.s,
},
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.3)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
},
loadingCard: {
backgroundColor: colors.surface,
borderRadius: 20,
padding: spacing.l,
alignItems: 'center',
...shadows.strong,
},
});