NeoScan_Physician/app/modules/AIPrediction/components/AIPredictionCard.tsx
2025-08-20 20:42:33 +05:30

523 lines
15 KiB
TypeScript

/*
* File: AIPredictionCard.tsx
* Description: Card component for displaying AI prediction case information
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Dimensions,
} from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { theme } from '../../../theme';
import { AIPredictionCase, URGENCY_COLORS, SEVERITY_COLORS, CATEGORY_COLORS } from '../types';
// ============================================================================
// INTERFACES
// ============================================================================
interface AIPredictionCardProps {
predictionCase: AIPredictionCase;
onPress: (predictionCase: AIPredictionCase) => void;
onReview?: (caseId: string) => void;
isSelected?: boolean;
onToggleSelect?: (caseId: string) => void;
showReviewButton?: boolean;
}
// ============================================================================
// CONSTANTS
// ============================================================================
const { width } = Dimensions.get('window');
const CARD_WIDTH = width - 32; // Full width with margins
// ============================================================================
// AI PREDICTION CARD COMPONENT
// ============================================================================
/**
* AIPredictionCard Component
*
* Purpose: Display AI prediction case information in a card format
*
* Features:
* - Patient ID and hospital information
* - AI prediction results with confidence score
* - Urgency and severity indicators
* - Review status and actions
* - Selection support for bulk operations
* - Modern card design with proper spacing
* - Color-coded priority indicators
* - Accessibility support
*/
const AIPredictionCard: React.FC<AIPredictionCardProps> = ({
predictionCase,
onPress,
onReview,
isSelected = false,
onToggleSelect,
showReviewButton = true,
}) => {
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/**
* Get Urgency Color
*
* Purpose: Get color based on clinical urgency
*/
const getUrgencyColor = (urgency: string): string => {
return URGENCY_COLORS[urgency as keyof typeof URGENCY_COLORS] || theme.colors.textMuted;
};
/**
* Get Severity Color
*
* Purpose: Get color based on primary severity
*/
const getSeverityColor = (severity: string): string => {
return SEVERITY_COLORS[severity as keyof typeof SEVERITY_COLORS] || theme.colors.textMuted;
};
/**
* Get Category Color
*
* Purpose: Get color based on finding category
*/
const getCategoryColor = (category: string): string => {
return CATEGORY_COLORS[category as keyof typeof CATEGORY_COLORS] || theme.colors.textMuted;
};
/**
* Get Review Status Color
*
* Purpose: Get color based on review status
*/
const getReviewStatusColor = (status: string): string => {
switch (status) {
case 'confirmed':
return theme.colors.success;
case 'reviewed':
return theme.colors.info;
case 'disputed':
return theme.colors.warning;
case 'pending':
default:
return theme.colors.error;
}
};
/**
* Format Confidence Score
*
* Purpose: Format confidence score as percentage
*/
const formatConfidence = (score: number): string => {
return `${Math.round(score * 100)}%`;
};
/**
* Capitalize Text
*
* Purpose: Capitalize first letter of each word
*/
const capitalize = (text: string): string => {
return text.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
};
/**
* Format Date
*
* Purpose: Format date for display
*/
const formatDate = (dateString?: string): string => {
if (!dateString) return 'N/A';
try {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
} catch {
return 'N/A';
}
};
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* Handle Card Press
*
* Purpose: Handle card tap to view details
*/
const handleCardPress = () => {
onPress(predictionCase);
};
/**
* Handle Review Press
*
* Purpose: Handle review button press
*/
const handleReviewPress = (event: any) => {
event.stopPropagation();
if (onReview) {
onReview(predictionCase.patid);
}
};
/**
* Handle Selection Toggle
*
* Purpose: Handle case selection toggle
*/
const handleSelectionToggle = (event: any) => {
event.stopPropagation();
if (onToggleSelect) {
onToggleSelect(predictionCase.patid);
}
};
// ============================================================================
// RENDER
// ============================================================================
return (
<TouchableOpacity
style={[
styles.container,
isSelected && styles.selectedContainer,
predictionCase.prediction.clinical_urgency === 'emergency' && styles.emergencyContainer,
]}
onPress={handleCardPress}
activeOpacity={0.7}
accessibilityRole="button"
accessibilityLabel={`AI Prediction case for patient ${predictionCase.patid}`}
accessibilityHint="Tap to view detailed prediction information"
>
{/* Header Section */}
<View style={styles.header}>
<View style={styles.headerLeft}>
<Text style={styles.patientId} numberOfLines={1}>
{predictionCase.patid}
</Text>
<Text style={styles.date}>
{formatDate(predictionCase.processed_at)}
</Text>
</View>
<View style={styles.headerRight}>
{onToggleSelect && (
<TouchableOpacity
style={styles.selectionButton}
onPress={handleSelectionToggle}
accessibilityRole="checkbox"
accessibilityState={{ checked: isSelected }}
>
<Icon
name={isSelected ? 'check-square' : 'square'}
size={20}
color={isSelected ? theme.colors.primary : theme.colors.textMuted}
/>
</TouchableOpacity>
)}
<View style={[
styles.priorityBadge,
{ backgroundColor: getUrgencyColor(predictionCase.prediction.clinical_urgency) }
]}>
<Text style={styles.priorityText}>
{capitalize(predictionCase.prediction.clinical_urgency)}
</Text>
</View>
</View>
</View>
{/* Prediction Information */}
<View style={styles.predictionSection}>
<View style={styles.predictionHeader}>
<Text style={styles.predictionLabel} numberOfLines={2}>
{capitalize(predictionCase.prediction.label)}
</Text>
<View style={styles.confidenceContainer}>
<Icon name="trending-up" size={16} color={theme.colors.primary} />
<Text style={styles.confidenceText}>
{formatConfidence(predictionCase.prediction.confidence_score)}
</Text>
</View>
</View>
{/* Finding Details */}
<View style={styles.findingDetails}>
<View style={styles.findingItem}>
<Text style={styles.findingLabel}>Type:</Text>
<Text style={styles.findingValue}>
{capitalize(predictionCase.prediction.finding_type)}
</Text>
</View>
<View style={styles.findingItem}>
<Text style={styles.findingLabel}>Category:</Text>
<View style={[
styles.categoryBadge,
{ backgroundColor: getCategoryColor(predictionCase.prediction.finding_category) }
]}>
<Text style={styles.categoryText}>
{capitalize(predictionCase.prediction.finding_category)}
</Text>
</View>
</View>
</View>
{/* Severity and Location */}
<View style={styles.detailsRow}>
<View style={styles.detailItem}>
<Icon name="alert-triangle" size={14} color={getSeverityColor(predictionCase.prediction.primary_severity)} />
<Text style={[styles.detailText, { color: getSeverityColor(predictionCase.prediction.primary_severity) }]}>
{capitalize(predictionCase.prediction.primary_severity)} Severity
</Text>
</View>
{predictionCase.prediction.anatomical_location !== 'not_applicable' && (
<View style={styles.detailItem}>
<Icon name="map-pin" size={14} color={theme.colors.textSecondary} />
<Text style={styles.detailText}>
{capitalize(predictionCase.prediction.anatomical_location)}
</Text>
</View>
)}
</View>
</View>
{/* Footer Section */}
<View style={styles.footer}>
<View style={styles.footerLeft}>
<View style={[
styles.reviewStatusBadge,
{ backgroundColor: getReviewStatusColor(predictionCase.review_status || 'pending') }
]}>
<Text style={styles.reviewStatusText}>
{capitalize(predictionCase.review_status || 'pending')}
</Text>
</View>
{predictionCase.reviewed_by && (
<Text style={styles.reviewedBy}>
by {predictionCase.reviewed_by}
</Text>
)}
</View>
{showReviewButton && predictionCase.review_status === 'pending' && (
<TouchableOpacity
style={styles.reviewButton}
onPress={handleReviewPress}
accessibilityRole="button"
accessibilityLabel="Review this case"
>
<Icon name="eye" size={16} color={theme.colors.primary} />
<Text style={styles.reviewButtonText}>Review</Text>
</TouchableOpacity>
)}
</View>
</TouchableOpacity>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.large,
padding: theme.spacing.lg,
marginHorizontal: theme.spacing.md,
marginVertical: theme.spacing.sm,
width: CARD_WIDTH,
...theme.shadows.medium,
borderWidth: 1,
borderColor: theme.colors.border,
},
selectedContainer: {
borderColor: theme.colors.primary,
borderWidth: 2,
},
emergencyContainer: {
borderLeftWidth: 4,
borderLeftColor: URGENCY_COLORS.emergency,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: theme.spacing.md,
},
headerLeft: {
flex: 1,
},
headerRight: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.sm,
},
patientId: {
fontSize: theme.typography.fontSize.bodyLarge,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
date: {
fontSize: theme.typography.fontSize.bodySmall,
color: theme.colors.textSecondary,
},
selectionButton: {
padding: theme.spacing.xs,
},
priorityBadge: {
paddingHorizontal: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
borderRadius: theme.borderRadius.small,
},
priorityText: {
fontSize: theme.typography.fontSize.caption,
fontWeight: theme.typography.fontWeight.medium,
color: theme.colors.background,
},
predictionSection: {
marginBottom: theme.spacing.md,
},
predictionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: theme.spacing.sm,
},
predictionLabel: {
fontSize: theme.typography.fontSize.bodyLarge,
fontWeight: theme.typography.fontWeight.medium,
color: theme.colors.textPrimary,
flex: 1,
marginRight: theme.spacing.sm,
},
confidenceContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.xs,
},
confidenceText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontWeight: theme.typography.fontWeight.medium,
color: theme.colors.primary,
},
findingDetails: {
marginBottom: theme.spacing.sm,
},
findingItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: theme.spacing.xs,
},
findingLabel: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textSecondary,
fontWeight: theme.typography.fontWeight.medium,
},
findingValue: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textPrimary,
},
categoryBadge: {
paddingHorizontal: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
borderRadius: theme.borderRadius.small,
},
categoryText: {
fontSize: theme.typography.fontSize.caption,
fontWeight: theme.typography.fontWeight.medium,
color: theme.colors.background,
},
detailsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
detailItem: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.xs,
flex: 1,
},
detailText: {
fontSize: theme.typography.fontSize.bodySmall,
color: theme.colors.textSecondary,
},
footer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: theme.spacing.sm,
borderTopWidth: 1,
borderTopColor: theme.colors.border,
},
footerLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.sm,
flex: 1,
},
reviewStatusBadge: {
paddingHorizontal: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
borderRadius: theme.borderRadius.small,
},
reviewStatusText: {
fontSize: theme.typography.fontSize.caption,
fontWeight: theme.typography.fontWeight.medium,
color: theme.colors.background,
},
reviewedBy: {
fontSize: theme.typography.fontSize.caption,
color: theme.colors.textMuted,
fontStyle: 'italic',
},
reviewButton: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.xs,
paddingHorizontal: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
borderRadius: theme.borderRadius.small,
borderWidth: 1,
borderColor: theme.colors.primary,
},
reviewButtonText: {
fontSize: theme.typography.fontSize.bodySmall,
color: theme.colors.primary,
fontWeight: theme.typography.fontWeight.medium,
},
});
export default AIPredictionCard;
/*
* End of File: AIPredictionCard.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/