NeoScan_Physician/app/modules/Dashboard/components/PredictionsList.tsx
2025-08-22 14:57:50 +05:30

405 lines
12 KiB
TypeScript

/*
* File: PredictionsList.tsx
* Description: Predictions list component with tabbed interface for feedback status
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useMemo, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
FlatList,
RefreshControl,
} from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { theme } from '../../../theme/theme';
import { PredictionCard } from './PredictionCard';
import { usePredictions } from '../hooks/usePredictions';
import type { PredictionData } from '../types/predictions';
// ============================================================================
// INTERFACES
// ============================================================================
interface PredictionsListProps {
onPredictionPress: (prediction: PredictionData) => void;
}
// ============================================================================
// PREDICTIONS LIST COMPONENT
// ============================================================================
/**
* PredictionsList Component
*
* Purpose: Display AI predictions organized by radiologist feedback status
*
* Features:
* - Two tabs: Radiologist Reviewed and Pending Review
* - Search functionality
* - Pull-to-refresh
* - Loading states
* - Error handling
* - Empty states
*
* Performance Optimizations:
* - Both datasets (with/without feedback) are pre-loaded upfront
* - Tab switching is instant - no filtering needed
* - React.memo prevents unnecessary re-renders
* - useCallback optimizes event handlers
*/
export const PredictionsList: React.FC<PredictionsListProps> = React.memo(({
onPredictionPress,
}) => {
const {
activeTab,
currentPredictions,
currentLoadingState,
currentError,
switchTab,
refreshPredictions,
} = usePredictions();
// Performance optimization: Memoize feedback count calculation
const feedbackCount = useMemo(() => {
if (activeTab === 'with-feedback') {
return currentPredictions.filter(p => p.feedbacks?.length > 0).length;
}
return 0;
}, [activeTab, currentPredictions]);
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
/**
* Handle Tab Switch
*
* Purpose: Switch between radiologist feedback status tabs
*/
const handleTabSwitch = useCallback((tab: 'with-feedback' | 'without-feedback') => {
switchTab(tab);
}, [switchTab]);
/**
* Handle Prediction Press
*
* Purpose: Handle when a prediction card is pressed
*/
const handlePredictionPress = useCallback((prediction: PredictionData) => {
onPredictionPress(prediction);
}, [onPredictionPress]);
/**
* Render Tab Button
*
* Purpose: Render individual radiologist feedback status tab button
*/
const renderTabButton = (tab: 'with-feedback' | 'without-feedback', label: string, icon: string) => {
const isActive = activeTab === tab;
return (
<TouchableOpacity
style={[styles.tabButton, isActive && styles.activeTabButton]}
onPress={() => handleTabSwitch(tab)}
activeOpacity={0.7}
>
<Icon
name={icon}
size={20}
color={isActive ? theme.colors.primary : theme.colors.textSecondary}
/>
<Text style={[styles.tabButtonText, isActive && styles.activeTabButtonText]}>
{label}
</Text>
</TouchableOpacity>
);
};
/**
* Render Empty State
*
* Purpose: Render empty state when no predictions available
*/
const renderEmptyState = () => (
<View style={styles.emptyState}>
<Icon name="inbox" size={48} color={theme.colors.textMuted} />
<Text style={styles.emptyStateTitle}>No Predictions Found</Text>
<Text style={styles.emptyStateSubtitle}>
{activeTab === 'with-feedback'
? 'No predictions have been reviewed by radiologists yet.'
: 'No predictions are waiting for radiologist review at the moment.'
}
</Text>
</View>
);
/**
* Render Error State
*
* Purpose: Render error state when API call fails
*/
const renderErrorState = () => (
<View style={styles.errorState}>
<Icon name="alert-circle" size={48} color={theme.colors.error} />
<Text style={styles.errorStateTitle}>Something went wrong</Text>
<Text style={styles.errorStateSubtitle}>
{currentError || 'Failed to load predictions. Please try again.'}
</Text>
<TouchableOpacity
style={styles.retryButton}
onPress={refreshPredictions}
activeOpacity={0.7}
>
<Icon name="refresh-cw" size={16} color={'white'} />
<Text style={styles.retryButtonText}>Retry</Text>
</TouchableOpacity>
</View>
);
// ============================================================================
// RENDER
// ============================================================================
// Performance optimization: Only log when needed (development mode)
// Note: Tab switching is now instant due to pre-loaded datasets
if (__DEV__ && currentPredictions.length > 0) {
console.log('🔍 PredictionsList render debug:', {
activeTab,
count: currentPredictions.length,
performance: 'Instant tab switching - datasets pre-loaded'
});
}
return (
<View style={styles.container}>
{/* Tab Navigation */}
<View style={styles.tabContainer}>
{renderTabButton('with-feedback', 'Radiologist Reviewed', 'message-circle')}
{renderTabButton('without-feedback', 'Pending Review', 'message-square')}
</View>
{/* Content Area */}
<View style={styles.contentContainer}>
{currentError ? (
renderErrorState()
) : (
<>
{/* Fixed Header - Not part of scrolling */}
{currentPredictions.length > 0 && (
<View style={styles.listHeader}>
<Text style={styles.listHeaderTitle}>
{activeTab === 'with-feedback' ? 'Radiologist Reviewed Predictions' : 'Predictions Awaiting Review'}
</Text>
<Text style={styles.listHeaderSubtitle}>
{currentPredictions.length} prediction{currentPredictions.length !== 1 ? 's' : ''} found
{activeTab === 'with-feedback' && (
<Text style={styles.feedbackCount}>
{' • '}{feedbackCount} with feedback
</Text>
)}
</Text>
</View>
)}
{/* Horizontal Scrolling Predictions */}
<View style={styles.listWrapper}>
<FlatList
data={currentPredictions}
renderItem={({ item }) => (
<View style={styles.predictionCardWrapper}>
<PredictionCard
prediction={item}
onPress={() => handlePredictionPress(item)}
/>
</View>
)}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={styles.listContainer}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
horizontal={true}
scrollEnabled={true}
refreshControl={
<RefreshControl
refreshing={currentLoadingState}
onRefresh={refreshPredictions}
colors={[theme.colors.primary]}
tintColor={theme.colors.primary}
/>
}
ListEmptyComponent={renderEmptyState}
/>
</View>
</>
)}
</View>
</View>
);
});
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.background,
flex: 1,
},
tabContainer: {
flexDirection: 'row',
backgroundColor: theme.colors.background,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.sm,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
tabButton: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.sm,
borderRadius: theme.borderRadius.medium,
gap: theme.spacing.xs,
},
activeTabButton: {
backgroundColor: theme.colors.tertiary,
},
tabButtonText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textSecondary,
},
activeTabButtonText: {
color: theme.colors.primary,
},
contentContainer: {
flex: 1,
minHeight: 0,
},
listWrapper: {
// flex: 1,
minHeight: 0, // Prevent flex overflow
marginBottom: theme.spacing.md, // Add bottom margin for shadow visibility
},
listContainer: {
padding: 2,
paddingBottom: theme.spacing.xxl, // Increased bottom padding for shadow visibility
alignItems: 'flex-start',
},
listHeader: {
marginBottom: theme.spacing.md,
paddingBottom: theme.spacing.md,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
width: '100%',
},
listHeaderTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
listHeaderSubtitle: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
},
feedbackCount: {
color: theme.colors.primary,
fontFamily: theme.typography.fontFamily.medium,
},
emptyState: {
justifyContent: 'center',
alignItems: 'center',
padding: theme.spacing.xxl,
},
emptyStateTitle: {
fontSize: theme.typography.fontSize.displayMedium,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginTop: theme.spacing.md,
marginBottom: theme.spacing.sm,
textAlign: 'center',
},
emptyStateSubtitle: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
textAlign: 'center',
},
errorState: {
justifyContent: 'center',
alignItems: 'center',
padding: theme.spacing.xxl,
},
errorStateTitle: {
fontSize: theme.typography.fontSize.displayMedium,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginTop: theme.spacing.md,
marginBottom: theme.spacing.sm,
textAlign: 'center',
},
errorStateSubtitle: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
textAlign: 'center',
marginBottom: theme.spacing.lg,
},
retryButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.colors.primary,
paddingHorizontal: theme.spacing.lg,
paddingVertical: theme.spacing.md,
borderRadius: theme.borderRadius.medium,
gap: theme.spacing.sm,
},
retryButtonText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.background,
},
predictionCardWrapper: {
marginRight: theme.spacing.md,
marginBottom: theme.spacing.md, // Add bottom margin for shadow visibility
width: 280, // Uniform width for all cards
// Height will be determined by content naturally
},
});
/*
* End of File: PredictionsList.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/