523 lines
15 KiB
TypeScript
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.
|
|
*/
|