NeoScan_Radiologist/app/modules/PatientCare/components/PatientCard.tsx
2025-08-12 18:50:19 +05:30

485 lines
13 KiB
TypeScript

/*
* File: PatientCard.tsx
* Description: 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 { MedicalCase, PatientDetails, Series } from '../../../shared/types';
// ============================================================================
// INTERFACES
// ============================================================================
interface PatientCardProps {
patient: MedicalCase;
onPress: () => void;
onEmergencyPress?: () => void;
}
// ============================================================================
// PATIENT CARD COMPONENT
// ============================================================================
/**
* PatientCard Component
*
* Purpose: Display DICOM medical case information in a card format
*
* Features:
* - Patient basic information from DICOM data
* - Modality and institution information
* - Case type with color coding
* - Series information
* - Time since created
* - Emergency alert for critical cases
* - Modern ER-focused design
*/
const PatientCard: React.FC<PatientCardProps> = ({
patient,
onPress,
onEmergencyPress,
}) => {
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
/**
* Parse JSON strings safely
*
* Purpose: Handle JSON string or object parsing for patient data
*
* @param jsonString - JSON string or object
* @returns Parsed object or empty object
*/
const parseJsonSafely = (jsonString: string | object) => {
if (typeof jsonString === 'object') {
return jsonString;
}
if (typeof jsonString === 'string') {
try {
return JSON.parse(jsonString);
} catch (error) {
console.warn('Failed to parse JSON:', error);
return {};
}
}
return {};
};
/**
* Get Case Type Color Configuration
*
* Purpose: Get color and icon based on case type
*
* @param type - Case type
* @returns Color configuration object
*/
const getCaseTypeConfig = (type: string) => {
switch (type) {
case 'Critical':
return {
color: theme.colors.error,
icon: 'alert-triangle',
bgColor: '#FFF5F5'
};
case 'Emergency':
return {
color: '#FF8C00',
icon: 'alert-circle',
bgColor: '#FFF8E1'
};
case 'Routine':
return {
color: theme.colors.success,
icon: 'check-circle',
bgColor: '#F0FFF4'
};
default:
return {
color: theme.colors.primary,
icon: 'info',
bgColor: theme.colors.background
};
}
};
/**
* Get Modality Color
*
* Purpose: Get color based on imaging modality
*
* @param modality - Imaging modality
* @returns Color code
*/
const getModalityColor = (modality: string) => {
switch (modality) {
case 'CT':
return '#4A90E2';
case 'MR':
return '#7B68EE';
case 'DX':
return '#50C878';
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'
});
};
// ============================================================================
// DATA EXTRACTION
// ============================================================================
const patientDetails = parseJsonSafely(patient.patientdetails);
const patientData = patientDetails.patientdetails || patientDetails;
const series = parseJsonSafely(patientDetails.series);
const typeConfig = getCaseTypeConfig(patient.type);
// ============================================================================
// RENDER HELPERS
// ============================================================================
/**
* Render Case Type Badge
*
* Purpose: Render case type indicator badge
*/
const renderTypeBadge = () => (
<View style={[styles.typeBadge, { backgroundColor: typeConfig.color }]}>
<Icon name={typeConfig.icon} size={12} color={theme.colors.background} />
<Text style={styles.typeText}>{patient.type}</Text>
</View>
);
/**
* Render Emergency Button
*
* Purpose: Render emergency alert button for critical cases
*/
const renderEmergencyButton = () => {
if (patient.type !== 'Critical') {
return null;
}
return (
<TouchableOpacity
style={styles.emergencyButton}
onPress={onEmergencyPress}
activeOpacity={0.7}
>
<Icon name="alert-triangle" size={14} color={theme.colors.background} />
<Text style={styles.emergencyButtonText}>ALERT</Text>
</TouchableOpacity>
);
};
// ============================================================================
// MAIN RENDER
// ============================================================================
return (
<TouchableOpacity
style={[
styles.container,
// patient.type === 'Critical' && styles.containerCritical,
{ borderLeftColor: typeConfig.color }
]}
onPress={onPress}
activeOpacity={0.7}
>
{/* Header Section */}
<View style={styles.header}>
<View style={styles.headerLeft}>
<Text style={styles.patientName}>
{patientData.Name || 'Unknown Patient'}
</Text>
<Text style={styles.patientInfo}>
ID: {patientData.PatID || 'N/A'} {patientData.PatAge || 'N/A'}y {patientData.PatSex || 'N/A'}
</Text>
</View>
<View style={styles.headerRight}>
{renderTypeBadge()}
{renderEmergencyButton()}
</View>
</View>
{/* Medical Information Section */}
<View style={styles.medicalSection}>
<View style={styles.infoRow}>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>Modality</Text>
<Text style={[
styles.infoValue,
styles.modalityText,
{ color: getModalityColor(patientData.Modality) }
]}>
{patientData.Modality || 'N/A'}
</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>Status</Text>
<Text style={[
styles.infoValue,
{ color: patientData.Status === 'Active' ? theme.colors.success : theme.colors.textSecondary }
]}>
{patientData.Status || 'Unknown'}
</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>Report</Text>
<Text style={[
styles.infoValue,
{ color: patientData.ReportStatus === 'Completed' ? theme.colors.success : theme.colors.warning }
]}>
{patientData.ReportStatus || 'Pending'}
</Text>
</View>
</View>
{/* Institution */}
<View style={styles.institutionRow}>
<Icon name="home" size={14} color={theme.colors.textSecondary} />
<Text style={styles.institutionText}>
{patientData.InstName || 'Unknown Institution'}
</Text>
</View>
</View>
{/* Series Information */}
<View style={styles.seriesSection}>
<View style={styles.seriesHeader}>
<Icon name="layers" size={14} color={theme.colors.textSecondary} />
<Text style={styles.seriesLabel}>Series Information</Text>
</View>
<Text style={styles.seriesText}>
{Array.isArray(series) ? series.length : 0} Series Available
</Text>
</View>
{/* Footer */}
<View style={styles.footer}>
<Text style={styles.dateText}>
{formatDate(patient.created_at)}
</Text>
<View style={styles.footerRight}>
<Text style={styles.caseId}>Case #{patient.id}</Text>
<Icon name="chevron-right" size={16} color={theme.colors.textMuted} />
</View>
</View>
</TouchableOpacity>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.background,
borderRadius: 12,
padding: theme.spacing.md,
marginHorizontal: theme.spacing.md,
marginVertical: theme.spacing.xs,
shadowColor: theme.colors.shadow,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
borderWidth: 1,
borderColor: theme.colors.border,
borderLeftWidth: 4,
},
containerCritical: {
borderColor: theme.colors.error,
borderWidth: 2,
backgroundColor: '#FFF5F5',
},
// Header Section
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: theme.spacing.sm,
},
headerLeft: {
flex: 1,
marginRight: theme.spacing.sm,
},
headerRight: {
flexDirection: 'row',
alignItems: 'center',
},
patientName: {
fontSize: 18,
fontWeight: 'bold',
color: theme.colors.textPrimary,
fontFamily: theme.typography.fontFamily.bold,
},
patientInfo: {
fontSize: 14,
color: theme.colors.textSecondary,
marginTop: 2,
fontFamily: theme.typography.fontFamily.regular,
},
// Type Badge
typeBadge: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
marginRight: theme.spacing.xs,
},
typeText: {
fontSize: 10,
fontWeight: 'bold',
color: theme.colors.background,
marginLeft: 4,
textTransform: 'uppercase',
},
// Emergency Button
emergencyButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.colors.error,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
},
emergencyButtonText: {
fontSize: 10,
fontWeight: 'bold',
color: theme.colors.background,
marginLeft: 4,
},
// Medical Section
medicalSection: {
marginBottom: theme.spacing.sm,
paddingBottom: theme.spacing.sm,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: theme.spacing.sm,
},
infoItem: {
flex: 1,
alignItems: 'center',
},
infoLabel: {
fontSize: 10,
color: theme.colors.textMuted,
marginBottom: 2,
textTransform: 'uppercase',
fontWeight: '500',
},
infoValue: {
fontSize: 14,
fontWeight: '600',
color: theme.colors.textPrimary,
textAlign: 'center',
},
modalityText: {
fontWeight: 'bold',
},
// Institution Row
institutionRow: {
flexDirection: 'row',
alignItems: 'center',
},
institutionText: {
fontSize: 14,
color: theme.colors.textSecondary,
marginLeft: 6,
flex: 1,
fontFamily: theme.typography.fontFamily.regular,
},
// Series Section
seriesSection: {
backgroundColor: theme.colors.backgroundAlt,
borderRadius: 8,
padding: theme.spacing.sm,
marginBottom: theme.spacing.sm,
},
seriesHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 4,
},
seriesLabel: {
fontSize: 12,
fontWeight: '500',
color: theme.colors.textSecondary,
marginLeft: 4,
},
seriesText: {
fontSize: 14,
color: theme.colors.textPrimary,
fontWeight: '500',
},
// Footer Section
footer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
dateText: {
fontSize: 12,
color: theme.colors.textMuted,
fontFamily: theme.typography.fontFamily.regular,
},
footerRight: {
flexDirection: 'row',
alignItems: 'center',
},
caseId: {
fontSize: 12,
color: theme.colors.textSecondary,
marginRight: theme.spacing.xs,
fontWeight: '500',
},
});
export default PatientCard;
/*
* End of File: PatientCard.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/