599 lines
16 KiB
TypeScript
599 lines
16 KiB
TypeScript
/*
|
|
* File: PatientCard.tsx
|
|
* Description: Enhanced patient card component for displaying DICOM medical case information
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/
|
|
|
|
import React from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
TouchableOpacity,
|
|
StyleSheet,
|
|
} from 'react-native';
|
|
import { theme } from '../../../theme/theme';
|
|
import Icon from 'react-native-vector-icons/Feather';
|
|
import { PatientData } from '../redux/patientCareSlice';
|
|
|
|
// ============================================================================
|
|
// INTERFACES
|
|
// ============================================================================
|
|
|
|
interface PatientCardProps {
|
|
patient: PatientData;
|
|
onPress: () => void;
|
|
onEmergencyPress?: () => void;
|
|
}
|
|
|
|
// ============================================================================
|
|
// PATIENT CARD COMPONENT
|
|
// ============================================================================
|
|
|
|
/**
|
|
* PatientCard Component
|
|
*
|
|
* Purpose: Display DICOM medical case information in a modern, enhanced card format
|
|
*
|
|
* Features:
|
|
* - Enhanced visual hierarchy with modern design
|
|
* - Improved status indicators and color coding
|
|
* - Better spacing and typography
|
|
* - Enhanced shadows and elevation
|
|
* - More intuitive information layout
|
|
* - Emergency alert for critical cases
|
|
* - Modern ER-focused design with better UX
|
|
*/
|
|
const PatientCard: React.FC<PatientCardProps> = ({
|
|
patient,
|
|
onPress,
|
|
onEmergencyPress,
|
|
}) => {
|
|
// ============================================================================
|
|
// UTILITY FUNCTIONS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get Status Color Configuration
|
|
*
|
|
* Purpose: Get enhanced color and icon based on processing status
|
|
*
|
|
* @param status - Processing status
|
|
* @returns Enhanced color configuration object
|
|
*/
|
|
const getStatusConfig = (status: string) => {
|
|
switch (status.toLowerCase()) {
|
|
case 'processed':
|
|
return {
|
|
color: '#10B981',
|
|
icon: 'check-circle',
|
|
bgColor: '#ECFDF5',
|
|
borderColor: '#D1FAE5'
|
|
};
|
|
case 'pending':
|
|
return {
|
|
color: '#F59E0B',
|
|
icon: 'clock',
|
|
bgColor: '#FFFBEB',
|
|
borderColor: '#FEF3C7'
|
|
};
|
|
case 'error':
|
|
return {
|
|
color: '#EF4444',
|
|
icon: 'alert-triangle',
|
|
bgColor: '#FEF2F2',
|
|
borderColor: '#FECACA'
|
|
};
|
|
default:
|
|
return {
|
|
color: '#3B82F6',
|
|
icon: 'info',
|
|
bgColor: '#EFF6FF',
|
|
borderColor: '#DBEAFE'
|
|
};
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get Modality Color
|
|
*
|
|
* Purpose: Get enhanced color based on imaging modality
|
|
*
|
|
* @param modality - Imaging modality
|
|
* @returns Enhanced color code
|
|
*/
|
|
const getModalityColor = (modality: string) => {
|
|
switch (modality.toUpperCase()) {
|
|
case 'CT':
|
|
return '#3B82F6';
|
|
case 'MR':
|
|
return '#8B5CF6';
|
|
case 'DX':
|
|
return '#10B981';
|
|
case 'DICOM':
|
|
return '#EF4444';
|
|
default:
|
|
return theme.colors.textSecondary;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Format Date
|
|
*
|
|
* Purpose: Format date string to readable format
|
|
*
|
|
* @param dateString - ISO date string
|
|
* @returns Formatted date string
|
|
*/
|
|
const formatDate = (dateString: string) => {
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Get Time Since Processed
|
|
*
|
|
* Purpose: Get human-readable time since last processed
|
|
*
|
|
* @param dateString - ISO date string
|
|
* @returns Formatted time string
|
|
*/
|
|
const getTimeSinceProcessed = (dateString: string) => {
|
|
const now = new Date();
|
|
const processed = new Date(dateString);
|
|
const diffInMinutes = Math.floor((now.getTime() - processed.getTime()) / (1000 * 60));
|
|
|
|
if (diffInMinutes < 1) return 'Just now';
|
|
if (diffInMinutes < 60) return `${diffInMinutes}m ago`;
|
|
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}h ago`;
|
|
return `${Math.floor(diffInMinutes / 1440)}d ago`;
|
|
};
|
|
|
|
// ============================================================================
|
|
// DATA EXTRACTION
|
|
// ============================================================================
|
|
|
|
const patientInfo = patient.patient_info;
|
|
const seriesCount = patient.series_summary.length;
|
|
const statusConfig = getStatusConfig(patientInfo.status);
|
|
const isCritical = patientInfo.report_status === 'Critical' || patientInfo.status === 'Error';
|
|
|
|
// ============================================================================
|
|
// RENDER HELPERS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Render Enhanced Status Badge
|
|
*
|
|
* Purpose: Render improved processing status indicator badge
|
|
*/
|
|
const renderStatusBadge = () => (
|
|
<View style={[styles.statusBadge, {
|
|
backgroundColor: statusConfig.bgColor,
|
|
borderColor: statusConfig.borderColor
|
|
}]}>
|
|
<Icon name={statusConfig.icon} size={14} color={statusConfig.color} />
|
|
<Text style={[styles.statusText, { color: statusConfig.color }]}>
|
|
{patientInfo.status}
|
|
</Text>
|
|
</View>
|
|
);
|
|
|
|
/**
|
|
* Render Enhanced Emergency Button
|
|
*
|
|
* Purpose: Render improved emergency alert button for critical cases
|
|
*/
|
|
const renderEmergencyButton = () => {
|
|
if (!isCritical) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
style={styles.emergencyButton}
|
|
onPress={onEmergencyPress}
|
|
activeOpacity={0.8}
|
|
>
|
|
<Icon name="alert-triangle" size={16} color={theme.colors.background} />
|
|
<Text style={styles.emergencyButtonText}>ALERT</Text>
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Render Enhanced Modality Badge
|
|
*
|
|
* Purpose: Render improved modality indicator
|
|
*/
|
|
const renderModalityBadge = () => (
|
|
<View style={[styles.modalityBadge, {
|
|
backgroundColor: getModalityColor(patientInfo.modality) + '20',
|
|
borderColor: getModalityColor(patientInfo.modality)
|
|
}]}>
|
|
<Text style={[styles.modalityText, {
|
|
color: getModalityColor(patientInfo.modality)
|
|
}]}>
|
|
{patientInfo.modality || 'N/A'}
|
|
</Text>
|
|
</View>
|
|
);
|
|
|
|
// ============================================================================
|
|
// MAIN RENDER
|
|
// ============================================================================
|
|
return (
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.container,
|
|
isCritical && styles.containerCritical,
|
|
{ borderLeftColor: statusConfig.color }
|
|
]}
|
|
onPress={onPress}
|
|
activeOpacity={0.8}
|
|
>
|
|
{/* Enhanced Header Section */}
|
|
<View style={styles.header}>
|
|
<View style={styles.headerLeft}>
|
|
<View style={styles.patientNameRow}>
|
|
<Text style={styles.patientName}>
|
|
{patientInfo.name || 'Unknown Patient'}
|
|
</Text>
|
|
{renderModalityBadge()}
|
|
</View>
|
|
<Text style={styles.patientInfo}>
|
|
ID: {patient.patid} • {patientInfo.age || 'N/A'}y • {patientInfo.sex || 'N/A'}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.headerRight}>
|
|
{renderStatusBadge()}
|
|
{renderEmergencyButton()}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Enhanced Medical Information Section */}
|
|
<View style={styles.medicalSection}>
|
|
<View style={styles.infoGrid}>
|
|
<View style={styles.infoCard}>
|
|
<Icon name="file-text" size={16} color={theme.colors.primary} />
|
|
<Text style={styles.infoValue}>
|
|
{patient.total_files_processed || 0}
|
|
</Text>
|
|
<Text style={styles.infoLabel}>Files</Text>
|
|
</View>
|
|
|
|
<View style={styles.infoCard}>
|
|
<Icon name="layers" size={16} color={theme.colors.warning} />
|
|
<Text style={styles.infoValue}>
|
|
{seriesCount}
|
|
</Text>
|
|
<Text style={styles.infoLabel}>Series</Text>
|
|
</View>
|
|
|
|
<View style={styles.infoCard}>
|
|
<Icon name="clipboard" size={16} color={
|
|
patientInfo.report_status === 'Available' ? theme.colors.success : theme.colors.warning
|
|
} />
|
|
<Text style={[styles.infoValue, {
|
|
color: patientInfo.report_status === 'Available' ? theme.colors.success : theme.colors.warning
|
|
}]}>
|
|
{patientInfo.report_status || 'Pending'}
|
|
</Text>
|
|
<Text style={styles.infoLabel}>Report</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Enhanced Institution Row */}
|
|
<View style={styles.institutionRow}>
|
|
<View style={styles.institutionIcon}>
|
|
<Icon name="home" size={16} color={theme.colors.primary} />
|
|
</View>
|
|
<Text style={styles.institutionText}>
|
|
{patientInfo.institution || 'Unknown Institution'}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Enhanced Series Information */}
|
|
<View style={styles.seriesSection}>
|
|
<View style={styles.seriesHeader}>
|
|
<Icon name="layers" size={16} color={theme.colors.textSecondary} />
|
|
<Text style={styles.seriesLabel}>Series Details</Text>
|
|
</View>
|
|
<View style={styles.seriesInfo}>
|
|
<Text style={styles.seriesText}>
|
|
{seriesCount} Series Available
|
|
</Text>
|
|
<Text style={styles.frameText}>
|
|
{patientInfo.frame_count || 0} Total Frames
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Enhanced Footer */}
|
|
<View style={styles.footer}>
|
|
<View style={styles.footerLeft}>
|
|
<Text style={styles.dateText}>
|
|
{formatDate(patientInfo.date)}
|
|
</Text>
|
|
<Text style={styles.processedText}>
|
|
Processed {getTimeSinceProcessed(patient.last_processed_at)}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.footerRight}>
|
|
<Text style={styles.caseId}>Case #{patient.patid}</Text>
|
|
<View style={styles.chevronContainer}>
|
|
<Icon name="chevron-right" size={18} color={theme.colors.primary} />
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
|
|
// ============================================================================
|
|
// ENHANCED STYLES
|
|
// ============================================================================
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
backgroundColor: theme.colors.background,
|
|
borderRadius: 16,
|
|
padding: theme.spacing.md,
|
|
marginHorizontal: theme.spacing.md,
|
|
marginVertical: theme.spacing.xs,
|
|
shadowColor: '#000000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.06,
|
|
shadowRadius: 8,
|
|
elevation: 3,
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
borderLeftWidth: 4,
|
|
},
|
|
containerCritical: {
|
|
borderColor: theme.colors.error,
|
|
borderWidth: 2,
|
|
backgroundColor: '#FEF2F2',
|
|
shadowColor: theme.colors.error,
|
|
shadowOpacity: 0.15,
|
|
},
|
|
|
|
// Enhanced Header Section
|
|
header: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-start',
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
headerLeft: {
|
|
flex: 1,
|
|
marginRight: theme.spacing.md,
|
|
},
|
|
headerRight: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: theme.spacing.sm,
|
|
},
|
|
patientNameRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: theme.spacing.xs,
|
|
gap: theme.spacing.xs,
|
|
},
|
|
patientName: {
|
|
fontSize: 20,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
flex: 1,
|
|
},
|
|
patientInfo: {
|
|
fontSize: 14,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
lineHeight: 20,
|
|
},
|
|
|
|
// Enhanced Status Badge
|
|
statusBadge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 6,
|
|
borderRadius: 20,
|
|
borderWidth: 1.5,
|
|
gap: theme.spacing.xs,
|
|
},
|
|
statusText: {
|
|
fontSize: 11,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.5,
|
|
},
|
|
|
|
// Enhanced Emergency Button
|
|
emergencyButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: theme.colors.error,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 6,
|
|
borderRadius: 20,
|
|
gap: theme.spacing.xs,
|
|
shadowColor: theme.colors.error,
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 4,
|
|
elevation: 3,
|
|
},
|
|
emergencyButtonText: {
|
|
fontSize: 11,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
color: theme.colors.background,
|
|
letterSpacing: 0.5,
|
|
},
|
|
|
|
// Enhanced Modality Badge
|
|
modalityBadge: {
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 4,
|
|
borderRadius: 12,
|
|
borderWidth: 1.5,
|
|
},
|
|
modalityText: {
|
|
fontSize: 10,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.5,
|
|
},
|
|
|
|
// Enhanced Medical Section
|
|
medicalSection: {
|
|
marginBottom: theme.spacing.sm,
|
|
paddingBottom: theme.spacing.sm,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: theme.colors.border,
|
|
},
|
|
infoGrid: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
marginBottom: theme.spacing.sm,
|
|
gap: theme.spacing.sm,
|
|
},
|
|
infoCard: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
paddingVertical: theme.spacing.xs,
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
borderRadius: 12,
|
|
paddingHorizontal: theme.spacing.sm,
|
|
gap: theme.spacing.xs,
|
|
},
|
|
infoLabel: {
|
|
fontSize: 10,
|
|
color: theme.colors.textMuted,
|
|
textTransform: 'uppercase',
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
letterSpacing: 0.5,
|
|
textAlign: 'center',
|
|
},
|
|
infoValue: {
|
|
fontSize: 14,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
color: theme.colors.textPrimary,
|
|
textAlign: 'center',
|
|
},
|
|
|
|
// Enhanced Institution Row
|
|
institutionRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
padding: theme.spacing.sm,
|
|
borderRadius: 12,
|
|
gap: theme.spacing.sm,
|
|
},
|
|
institutionIcon: {
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 16,
|
|
backgroundColor: theme.colors.primary + '20',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
institutionText: {
|
|
fontSize: 14,
|
|
color: theme.colors.textSecondary,
|
|
flex: 1,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
},
|
|
|
|
// Enhanced Series Section
|
|
seriesSection: {
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
borderRadius: 12,
|
|
padding: theme.spacing.sm,
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
seriesHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: theme.spacing.sm,
|
|
gap: theme.spacing.xs,
|
|
},
|
|
seriesLabel: {
|
|
fontSize: 13,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
color: theme.colors.textSecondary,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.5,
|
|
},
|
|
seriesInfo: {
|
|
gap: theme.spacing.xs,
|
|
},
|
|
seriesText: {
|
|
fontSize: 15,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
},
|
|
frameText: {
|
|
fontSize: 13,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
},
|
|
|
|
// Enhanced Footer Section
|
|
footer: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
paddingTop: theme.spacing.sm,
|
|
borderTopWidth: 1,
|
|
borderTopColor: theme.colors.border,
|
|
},
|
|
footerLeft: {
|
|
flex: 1,
|
|
},
|
|
dateText: {
|
|
fontSize: 13,
|
|
color: theme.colors.textMuted,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
marginBottom: 2,
|
|
},
|
|
processedText: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
},
|
|
footerRight: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: theme.spacing.sm,
|
|
},
|
|
caseId: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
},
|
|
chevronContainer: {
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 16,
|
|
backgroundColor: theme.colors.primary + '20',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
});
|
|
|
|
export default PatientCard;
|
|
|
|
/*
|
|
* End of File: PatientCard.tsx
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/ |