Files
ProcureLinc-Mobile-App/src/Screens/Tender/PurchaseTenderAllList.tsx
T
Abin ToppoandClaude Opus 4.8 95d8e0a925 perf: project-wide optimization pass + UI/theme fixes
Optimization (audited across src/, typecheck green):
- Crash guards: PurchaseAuction data?.result + stray text-node, AuctionItemDetail
  length>0, LiveAuction user?.contractorId, KYC params guard
- Hoisted render-defined components: Login MinimalInput (keyboard-focus bug),
  TenderFullDetail Section/InfoBox (subtree remount)
- Memoized hot paths: Spreadsheet ColumnHeaderRow/DataRow, list makeStyles,
  shared infinite-query select (stops per-render flatMap)
- Removed hot-path/in-list console logs; SkeletonList 10->1 shimmer

UI/theme: themed AppToast, redesigned sidebar drawer, list empty states,
KYC WebView cleanup

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:14:23 +05:30

162 lines
6.5 KiB
TypeScript

import React, { useState, useCallback, useEffect } from 'react';
import { View, Text, FlatList, ActivityIndicator } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import moment from 'moment';
import { useFetchPurchaseTender } from '../../Services/ApiCall/Hooks';
import PurchaseTenderTile from './PurchaseTenderTile';
import { useDispatch } from 'react-redux';
import { setTender } from '../../Redux/Slices/SelectedTender';
import { SearchInput } from '../../components/SearchInput';
import { Skeleton, EmptyState } from '../../components';
import { allBooleanProps } from './types/types';
import FilterModal from './Modal/FilterModal';
import colors from '../../Constant/Color';
import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated';
import NavigationString from '../../Constant/NavigationString';
const PurchaseTenderAllList = () => {
const navigation = useNavigation() as any;
const Dispatch = useDispatch()
const [allBoolean, setAllBoolean] = useState<allBooleanProps>({ isShowFilter: false })
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch }: any = useFetchPurchaseTender();
const [showRefreshBanner, setShowRefreshBanner] = useState(false);
useEffect(() => {
if (isRefetching) {
setShowRefreshBanner(true);
} else {
const timer = setTimeout(() => {
setShowRefreshBanner(false);
}, 1000);
return () => clearTimeout(timer);
}
}, [isRefetching]);
const format = 'DD-MM-YYYY HH:mm:ss';
function isTenderActive(item: any): boolean {
const now = moment();
const start = moment(item?.keyDates?.TenderBidding?.startDateTime, format);
const end = moment(item?.keyDates?.TenderBidding?.endDateTime, format);
if (!start.isValid() || !end.isValid()) return false;
return now.isBetween(start, end, undefined, '[]');
}
function isTenderEnd(item: any): boolean {
const now = moment();
const end = moment(item?.keyDates?.TenderBidding?.endDateTime, format);
if (!end.isValid()) return false;
return now.isAfter(end);
}
const loadMore = useCallback(() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
// fetchNextPage identity changes each render; gating flags are the real deps.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasNextPage, isFetchingNextPage]);
const renderFooter = useCallback(() => {
if (!isFetchingNextPage) return null;
return (
<View style={{ paddingVertical: 12 }}>
<ActivityIndicator size="small" color={colors.primary} />
</View>
);
}, [isFetchingNextPage]);
return (
<View style={{ flex: 1 }}>
<SearchInput onChange={() => { }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} 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 tenders
</Text>
</Animated.View>
)}
{isLoading ? <Skeleton />
: <FlatList
data={data?.result ?? []}
keyExtractor={(item) => String(item.tenderId || item.id)}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ flexGrow: 1, paddingBottom: 200, paddingHorizontal: 10 }}
renderItem={({ item }) => (
<PurchaseTenderTile
item={item}
onPressDetails={() => {
// LiveTenderDetail reads a flat tender object from route.params
// (route.params.tenderId / .tenderStatus), same as LiveTenderTile.
navigation.navigate(NavigationString.LiveTenderDetail, item);
}}
onStartBidding={() => {
Dispatch(setTender(item))
navigation.navigate(NavigationString.TenderBidding, { data: item })
}}
buttonEnable={isTenderActive(item)}
isTenderEnd={isTenderEnd(item)}
/>
)}
onEndReached={loadMore}
onEndReachedThreshold={0.6}
ListFooterComponent={renderFooter}
ListEmptyComponent={
<EmptyState
title="No purchased tenders found"
subtitle="Tenders you purchase will appear here."
/>
}
refreshing={isRefetching}
onRefresh={refetch}
/>}
{allBoolean.isShowFilter && <View style={{ flex: 1 }}>
<FilterModal visible={allBoolean.isShowFilter} onClose={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: false })) }} onApply={() => { }} />
</View>}
</View>
);
};
export default PurchaseTenderAllList;