Files
ProcureLinc-Mobile-App/src/Screens/Tender/Tender.tsx
T
Abin ToppoandClaude Fable 5 1d7646d938
CI / quality (push) Waiting to run
feat: architecture refactor — core layer, spreadsheet engine, envelope bidding, tests
- Add src/core/ (config, errors, events, http, observability, storage, query)
- Add src/Spreadsheet/ engine with native POI backend and Excel bridge
- Add Envelope C spreadsheet flow and tender bidding envelope modules
- Consolidate components into src/components/, remove legacy Component/ and Hooks/
- Add registration state/validation, auth session guard, profile edit modules
- Add unit tests across axios, redux, bidding, registration, encryption, core
- Add CI workflow, docs (architecture, audit, DSC API spec), keystore example
- Remove tracked keystore binary; ignore scripts.zip

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 10:20:22 +05:30

348 lines
10 KiB
TypeScript

import React, { useState, useRef, useMemo } from 'react';
import {
View,
StyleSheet,
TouchableOpacity,
Animated,
Dimensions,
} from 'react-native';
import { InsideHeader } from '../../components';
import {
useFetchLiveTender,
useFetchPurchaseTender,
} from '../../Services/ApiCall/Hooks';
import { ShoppingCart, Lock, Radio, Package } from 'lucide-react-native';
import { useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
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 = 48;
const HORIZONTAL_PADDING = 32;
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 reduxToken = useSelector((state: any) => state.token?.token);
const liveCount = liveTenderData?.pages?.[0]?.totalCount || 0;
const purchaseCount = purchaseTenderData?.pages?.[0]?.totalCount || 0;
const TAB_WIDTH = useMemo(() => {
const availableWidth = SCREEN_WIDTH - HORIZONTAL_PADDING - SWITCH_INSETS * 2;
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, {
toValue: index * TAB_WIDTH,
useNativeDriver: true,
friction: 8,
}).start();
};
const isAuctionUser = reduxToken?.userType?.toLowerCase() === 'auction';
const cartCount = SelectedPurchaseList.length;
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>
</View>
);
}
return (
<View style={styles.container}>
<InsideHeader title="Tender" showArrow />
{/* 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.tabText,
selectedTab === 'live' && (styles.activeTabText as any),
]}
>
Live {liveCount ? `(${liveCount})` : ''}
</AppText>
</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>
{/* Content */}
{selectedTab === 'live' ? <LiveTender /> : <PurchaseTenderAllList />}
{/* 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>
)}
</TouchableOpacity>
</View>
);
};
export default Tender;
/* ─── 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,
});