/* * File: PatientDetailsScreen.tsx * Description: Comprehensive patient details screen with DICOM image viewer * Design & Developed by Tech4Biz Solutions * Copyright (c) Spurrin Innovations. All rights reserved. */ import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, StatusBar, Alert, Dimensions, Image, FlatList, RefreshControl, } from 'react-native'; import { theme } from '../../../theme/theme'; import { useAppDispatch } from '../../../store/hooks'; import Icon from 'react-native-vector-icons/Feather'; import { SafeAreaView } from 'react-native-safe-area-context'; // Import types import { MedicalCase, PatientDetails, Series } from '../../../shared/types'; // Import components import { ImageViewer } from '../components'; import { API_CONFIG } from '../../../shared/utils'; // Get screen dimensions const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); // ============================================================================ // INTERFACES // ============================================================================ interface PatientDetailsScreenProps { navigation: any; route: { params: { patientId: string; patientName: string; medicalCase: MedicalCase; }; }; } interface ParsedPatientData { patientDetails: PatientDetails; series: Series[]; } // ============================================================================ // PATIENT DETAILS SCREEN COMPONENT // ============================================================================ /** * PatientDetailsScreen Component * * Purpose: Comprehensive patient details display with DICOM image viewer * * Features: * - Full patient demographic information * - Medical case details and status * - DICOM series information * - Image gallery with thumbnail previews * - Real-time data updates * - Emergency actions for critical cases * - Medical history and notes * - Modern healthcare-focused UI design * - Responsive layout for different screen sizes */ const PatientDetailsScreen: React.FC = ({ navigation, route }) => { // ============================================================================ // STATE MANAGEMENT // ============================================================================ const dispatch = useAppDispatch(); // Route parameters const { patientId, patientName, medicalCase } = route.params; // Local state const [isRefreshing, setIsRefreshing] = useState(false); const [selectedImageIndex, setSelectedImageIndex] = useState(0); const [showFullImage, setShowFullImage] = useState(false); const [activeTab, setActiveTab] = useState<'overview' | 'images' | 'history'>('overview'); // ============================================================================ // DATA PARSING & PROCESSING // ============================================================================ /** * Parse Patient Data * * Purpose: Safely parse JSON strings from medical case data */ const parsedData: ParsedPatientData = useMemo(() => { 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 {}; }; const patientDetails = parseJsonSafely(medicalCase.patientdetails); const series = parseJsonSafely(patientDetails.series); return { patientDetails: patientDetails.patientdetails || patientDetails, series: Array.isArray(series) ? series : [], }; }, [medicalCase]); // ============================================================================ // LIFECYCLE METHODS // ============================================================================ /** * Component Mount Effect * * Purpose: Initialize screen and set up navigation */ useEffect(() => { // Set navigation title navigation.setOptions({ title: patientName || 'Patient Details', headerShown: false, }); }, [navigation, patientName]); // ============================================================================ // EVENT HANDLERS // ============================================================================ /** * Handle Refresh * * Purpose: Pull-to-refresh functionality */ const handleRefresh = useCallback(async () => { setIsRefreshing(true); // TODO: Implement refresh logic setTimeout(() => { setIsRefreshing(false); }, 1000); }, []); /** * Handle Image Press * * Purpose: Open full-screen image viewer for selected series * * @param seriesIndex - Index of the series */ const handleImagePress = useCallback((seriesIndex: number) => { setSelectedImageIndex(seriesIndex); setShowFullImage(true); }, []); /** * Handle Close Image Viewer * * Purpose: Close full-screen image viewer */ const handleCloseImageViewer = useCallback(() => { setShowFullImage(false); }, []); /** * Get All Images from Series * * Purpose: Extract image paths from DICOM series pngpath */ const getAllImages = useCallback(() => { const images: string[] = []; parsedData.series.forEach(series => { // Use pngpath for actual image display if (series.pngpath && typeof series.pngpath === 'string') { images.push(series.pngpath); } }); return images; }, [parsedData.series]); /** * Get Series Info for Image Index * * Purpose: Get series information for a given image index */ const getSeriesInfoForImage = useCallback((imageIndex: number) => { if (imageIndex >= 0 && imageIndex < parsedData.series.length) { const series = parsedData.series[imageIndex]; return { seriesNum: series.SeriesNum || '1', seriesDesc: series.SerDes || 'Unnamed Series', imageInSeries: 1, totalInSeries: 1 }; } return { seriesNum: '1', seriesDesc: 'Unknown Series', imageInSeries: 1, totalInSeries: 1 }; }, [parsedData.series]); /** * Handle Emergency Action * * Purpose: Handle emergency actions for critical patients */ const handleEmergencyAction = useCallback(() => { Alert.alert( 'Emergency Action Required', `Patient ${parsedData.patientDetails.Name} requires immediate attention`, [ { text: 'Call Code Blue', style: 'destructive', onPress: () => { // TODO: Implement emergency code calling Alert.alert('Emergency', 'Code Blue activated'); }, }, { text: 'Alert Team', onPress: () => { // TODO: Implement team alerting Alert.alert('Alert', 'Team notified'); }, }, { text: 'Cancel', style: 'cancel', }, ] ); }, [parsedData.patientDetails.Name]); /** * Handle Back Navigation * * Purpose: Navigate back to previous screen */ const handleBackPress = useCallback(() => { navigation.goBack(); }, [navigation]); // ============================================================================ // RENDER HELPERS // ============================================================================ /** * Render Patient Header * * Purpose: Render patient identification and status section */ const renderPatientHeader = () => ( {parsedData.patientDetails.Name || 'Unknown Patient'} MRN: {parsedData.patientDetails.PatID || 'N/A'} {parsedData.patientDetails.PatAge || 'N/A'} • {parsedData.patientDetails.PatSex || 'N/A'} {medicalCase.type} {medicalCase.type === 'Critical' && ( EMERGENCY )} ); /** * Render Tab Navigation * * Purpose: Render tab navigation for different sections */ const renderTabNavigation = () => ( {[ { key: 'overview', label: 'Overview', icon: 'info' }, { key: 'images', label: 'Images', icon: 'image', count: parsedData.series.length }, { key: 'history', label: 'History', icon: 'clock' }, ].map((tab) => ( setActiveTab(tab.key as any)} > {tab.label} {tab.count !== undefined && ( {tab.count} )} ))} ); /** * Render Overview Tab * * Purpose: Render patient overview information */ const renderOverviewTab = () => ( {/* Medical Case Information */} Case Information Case ID {medicalCase.id} Type {medicalCase.type} Created {new Date(medicalCase.created_at).toLocaleDateString()} Updated {new Date(medicalCase.updated_at).toLocaleDateString()} {/* Patient Details */} Patient Details Name {parsedData.patientDetails.Name || 'N/A'} Age {parsedData.patientDetails.PatAge || 'N/A'} Sex {parsedData.patientDetails.PatSex || 'N/A'} Status {parsedData.patientDetails.Status || 'N/A'} Institution {parsedData.patientDetails.InstName || 'N/A'} Modality {parsedData.patientDetails.Modality || 'N/A'} {/* Series Information */} {parsedData.series.length > 0 && ( Imaging Series {parsedData.series.length} series available Total series: {parsedData.series.length} )} ); /** * Render Images Tab * * Purpose: Render DICOM image gallery */ const renderImagesTab = () => ( {parsedData.series.length === 0 ? ( No Images Available No DICOM images are currently available for this patient ) : ( DICOM Images {parsedData.series.map((series, seriesIndex) => ( Series {series.SeriesNum}: {series.SerDes || 'Unnamed Series'} {series.ImgTotalinSeries || '0'} images in Path • {series.ViePos || 'Unknown position'} {/* Series Details */} Series Number: {series.SeriesNum || 'N/A'} Total Images: {series.ImgTotalinSeries || '0'} Description: {series.SerDes || 'N/A'} {series.Path && Array.isArray(series.Path) && ( Path Array: {series.Path.length} URLs available )} {series.ViePos && ( View Position: {series.ViePos} )} {/* Series Image */} {series.pngpath ? ( handleImagePress(seriesIndex)} > Series Image ) : ( No Image Available )} ))} )} ); /** * Render History Tab * * Purpose: Render patient medical history */ const renderHistoryTab = () => ( Medical History Case created on {new Date(medicalCase.created_at).toLocaleDateString()} Last updated on {new Date(medicalCase.updated_at).toLocaleDateString()} Status: {medicalCase.type} case Notes No additional notes available for this patient case. ); /** * Get Status Color * * Purpose: Get appropriate color for patient status * * @param status - Patient status */ const getStatusColor = (status: string) => { switch (status) { case 'Critical': return theme.colors.error; case 'Emergency': return theme.colors.warning; case 'Routine': return theme.colors.success; default: return theme.colors.info; } }; // ============================================================================ // MAIN RENDER // ============================================================================ return ( {/* Header */} Patient Details Emergency Department {/* Patient Header */} {renderPatientHeader()} {/* Tab Navigation */} {renderTabNavigation()} {/* Tab Content */} } > {activeTab === 'overview' && renderOverviewTab()} {activeTab === 'images' && renderImagesTab()} {activeTab === 'history' && renderHistoryTab()} {/* Full-Screen Image Viewer */} ); }; // ============================================================================ // STYLES // ============================================================================ const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: theme.colors.background, }, // Header Styles header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: theme.spacing.md, paddingVertical: theme.spacing.sm, backgroundColor: theme.colors.background, borderBottomWidth: 1, borderBottomColor: theme.colors.border, }, backButton: { padding: theme.spacing.sm, marginRight: theme.spacing.sm, }, headerTitle: { flex: 1, alignItems: 'center', }, headerTitleText: { fontSize: 18, fontWeight: 'bold', color: theme.colors.textPrimary, fontFamily: theme.typography.fontFamily.bold, }, headerSubtitleText: { fontSize: 12, color: theme.colors.textSecondary, fontFamily: theme.typography.fontFamily.regular, }, refreshButton: { padding: theme.spacing.sm, marginLeft: theme.spacing.sm, }, // Patient Header Styles patientHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: theme.spacing.md, paddingVertical: theme.spacing.lg, backgroundColor: theme.colors.background, borderBottomWidth: 1, borderBottomColor: theme.colors.border, }, patientInfo: { flexDirection: 'row', alignItems: 'center', flex: 1, }, patientAvatar: { width: 60, height: 60, borderRadius: 30, backgroundColor: theme.colors.backgroundAlt, justifyContent: 'center', alignItems: 'center', marginRight: theme.spacing.md, }, patientDetails: { flex: 1, }, patientName: { fontSize: 20, fontWeight: 'bold', color: theme.colors.textPrimary, fontFamily: theme.typography.fontFamily.bold, marginBottom: 4, }, patientId: { fontSize: 14, color: theme.colors.textSecondary, fontFamily: theme.typography.fontFamily.medium, marginBottom: 8, }, patientMeta: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, patientMetaText: { fontSize: 14, color: theme.colors.textSecondary, fontFamily: theme.typography.fontFamily.regular, }, statusBadge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16, }, statusText: { fontSize: 12, fontWeight: 'bold', color: theme.colors.background, textTransform: 'uppercase', }, emergencyButton: { flexDirection: 'row', alignItems: 'center', backgroundColor: theme.colors.error, paddingHorizontal: 16, paddingVertical: 12, borderRadius: 8, shadowColor: theme.colors.error, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.3, shadowRadius: 4, elevation: 4, }, emergencyButtonText: { color: theme.colors.background, fontSize: 12, fontWeight: 'bold', marginLeft: 8, textTransform: 'uppercase', }, // Tab Navigation Styles tabContainer: { flexDirection: 'row', backgroundColor: theme.colors.background, borderBottomWidth: 1, borderBottomColor: theme.colors.border, }, tabButton: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: theme.spacing.md, paddingHorizontal: theme.spacing.sm, position: 'relative', }, activeTabButton: { borderBottomWidth: 2, borderBottomColor: theme.colors.primary, }, tabLabel: { fontSize: 14, color: theme.colors.textSecondary, fontFamily: theme.typography.fontFamily.medium, marginLeft: 8, }, activeTabLabel: { color: theme.colors.primary, fontFamily: theme.typography.fontFamily.bold, }, tabCount: { backgroundColor: theme.colors.primary, borderRadius: 10, paddingHorizontal: 6, paddingVertical: 2, marginLeft: 8, }, tabCountText: { color: theme.colors.background, fontSize: 10, fontWeight: 'bold', }, // Content Styles content: { flex: 1, }, tabContent: { padding: theme.spacing.md, }, // Section Styles section: { marginBottom: theme.spacing.lg, }, sectionTitle: { fontSize: 18, fontWeight: 'bold', color: theme.colors.textPrimary, fontFamily: theme.typography.fontFamily.bold, marginBottom: theme.spacing.md, }, // Info Grid Styles infoGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', }, infoItem: { width: '48%', marginBottom: theme.spacing.md, }, infoLabel: { fontSize: 12, color: theme.colors.textSecondary, fontFamily: theme.typography.fontFamily.medium, marginBottom: 4, textTransform: 'uppercase', }, infoValue: { fontSize: 14, color: theme.colors.textPrimary, fontFamily: theme.typography.fontFamily.regular, }, // Series Info Styles seriesInfo: { backgroundColor: theme.colors.backgroundAlt, padding: theme.spacing.md, borderRadius: 8, }, seriesText: { fontSize: 14, color: theme.colors.textSecondary, fontFamily: theme.typography.fontFamily.regular, marginBottom: 4, }, // Images Styles imagesContainer: { marginTop: theme.spacing.sm, }, seriesContainer: { marginBottom: theme.spacing.lg, }, seriesHeader: { marginBottom: theme.spacing.md, }, seriesTitle: { fontSize: 16, fontWeight: 'bold', color: theme.colors.textPrimary, fontFamily: theme.typography.fontFamily.bold, marginBottom: 4, }, seriesMeta: { fontSize: 14, color: theme.colors.textSecondary, fontFamily: theme.typography.fontFamily.regular, }, imageList: { paddingRight: theme.spacing.md, }, imageThumbnail: { width: 120, height: 120, borderRadius: 8, marginRight: theme.spacing.sm, position: 'relative', shadowColor: '#000000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2, }, thumbnailImage: { width: '100%', height: '100%', borderRadius: 8, }, imageOverlay: { position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: 'rgba(0, 0, 0, 0.7)', paddingVertical: 4, paddingHorizontal: 8, borderBottomLeftRadius: 8, borderBottomRightRadius: 8, }, imageNumber: { color: theme.colors.background, fontSize: 10, fontWeight: 'bold', textAlign: 'center', }, // History Styles historyItem: { flexDirection: 'row', alignItems: 'center', marginBottom: theme.spacing.sm, }, historyText: { fontSize: 14, color: theme.colors.textSecondary, fontFamily: theme.typography.fontFamily.regular, marginLeft: theme.spacing.sm, }, notesText: { fontSize: 14, color: theme.colors.textSecondary, fontFamily: theme.typography.fontFamily.regular, lineHeight: 20, }, // Empty State Styles emptyState: { alignItems: 'center', justifyContent: 'center', paddingVertical: theme.spacing.xl, }, emptyStateTitle: { fontSize: 18, fontWeight: 'bold', color: theme.colors.textPrimary, fontFamily: theme.typography.fontFamily.bold, marginTop: theme.spacing.md, marginBottom: theme.spacing.sm, }, emptyStateSubtitle: { fontSize: 14, color: theme.colors.textSecondary, fontFamily: theme.typography.fontFamily.regular, textAlign: 'center', lineHeight: 20, }, // No Image Placeholder Styles noImagePlaceholder: { width: 120, height: 120, borderRadius: 8, marginRight: theme.spacing.sm, backgroundColor: theme.colors.backgroundAlt, justifyContent: 'center', alignItems: 'center', borderWidth: 1, borderColor: theme.colors.border, borderStyle: 'dashed', }, noImageText: { fontSize: 10, color: theme.colors.textMuted, fontFamily: theme.typography.fontFamily.regular, textAlign: 'center', marginTop: 4, }, // Series Details Styles seriesDetails: { backgroundColor: theme.colors.backgroundAlt, padding: theme.spacing.sm, borderRadius: 8, marginBottom: theme.spacing.md, }, seriesDetailItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: theme.spacing.xs, }, seriesDetailLabel: { fontSize: 12, color: theme.colors.textSecondary, fontFamily: theme.typography.fontFamily.medium, flex: 1, }, seriesDetailValue: { fontSize: 12, color: theme.colors.textPrimary, fontFamily: theme.typography.fontFamily.regular, flex: 2, textAlign: 'right', }, }); export default PatientDetailsScreen; /* * End of File: PatientDetailsScreen.tsx * Design & Developed by Tech4Biz Solutions * Copyright (c) Spurrin Innovations. All rights reserved. */