405 lines
12 KiB
TypeScript
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.
|
|
*/
|