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

618 lines
17 KiB
TypeScript

/*
* File: PatientsScreen.tsx
* Description: Main patients screen with search, filtering, and patient list
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useEffect, useState, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
RefreshControl,
TouchableOpacity,
StatusBar,
Alert,
FlatList,
Dimensions,
} from 'react-native';
import { theme } from '../../../theme/theme';
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
import Icon from 'react-native-vector-icons/Feather';
import { SafeAreaView } from 'react-native-safe-area-context';
// Import patient care functionality
import {
fetchPatients,
setSearchQuery,
setFilter,
setSort,
clearError
} from '../redux/patientCareSlice';
// Import patient care selectors
import {
selectPatients,
selectPatientsLoading,
selectPatientsError,
selectIsRefreshing,
selectSearchQuery,
selectSelectedFilter,
selectSortBy,
selectFilteredPatients,
} from '../redux/patientCareSelectors';
// Import auth selectors
import { selectUser } from '../../Auth/redux/authSelectors';
// Import components
import PatientCard from '../components/PatientCard';
import SearchBar from '../components/SearchBar';
import FilterTabs from '../components/FilterTabs';
import EmptyState from '../components/EmptyState';
import LoadingState from '../components/LoadingState';
// Import types
import { MedicalCase, PatientDetails, Series } from '../../../shared/types';
import { PatientsScreenProps } from '../navigation/navigationTypes';
// Get screen dimensions
const { width: screenWidth } = Dimensions.get('window');
// ============================================================================
// INTERFACES
// ============================================================================
// ============================================================================
// PATIENTS SCREEN COMPONENT
// ============================================================================
/**
* PatientsScreen Component
*
* Purpose: Main screen for displaying and managing patient list
*
* Features:
* - Real-time patient data fetching
* - Search functionality with real-time filtering
* - Filter tabs (All, Active, Critical, Discharged)
* - Sort options (Priority, Name, Date)
* - Pull-to-refresh functionality
* - Patient cards with vital information
* - Navigation to patient details
* - Loading and error states
* - Empty state handling
* - Modern ER-focused UI design
*/
const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
// ============================================================================
// STATE MANAGEMENT
// ============================================================================
const dispatch = useAppDispatch();
// Redux state
const patients = useAppSelector(selectPatients);
const filteredPatients = useAppSelector(selectFilteredPatients);
const isLoading = useAppSelector(selectPatientsLoading);
const isRefreshing = useAppSelector(selectIsRefreshing);
const error = useAppSelector(selectPatientsError);
const searchQuery = useAppSelector(selectSearchQuery);
const selectedFilter = useAppSelector(selectSelectedFilter);
const sortBy = useAppSelector(selectSortBy);
const user = useAppSelector(selectUser);
// Local state
const [showSortModal, setShowSortModal] = useState(false);
// ============================================================================
// LIFECYCLE METHODS
// ============================================================================
/**
* Component Mount Effect
*
* Purpose: Initialize screen and fetch patient data
*/
useEffect(() => {
// Fetch patients on mount
handleFetchPatients();
// Set up navigation focus listener for real-time updates
const unsubscribe = navigation.addListener('focus', () => {
handleRefresh();
});
return unsubscribe;
}, [navigation]);
/**
* Error Handling Effect
*
* Purpose: Display error alerts and clear errors
*/
useEffect(() => {
if (error) {
Alert.alert(
'Error',
error,
[
{
text: 'Retry',
onPress: handleFetchPatients,
},
{
text: 'OK',
onPress: () => dispatch(clearError()),
},
]
);
}
}, [error]);
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* Handle Fetch Patients
*
* Purpose: Fetch patients from API
*/
const handleFetchPatients = useCallback(() => {
if (user?.access_token) {
dispatch(fetchPatients(user.access_token));
}
}, [dispatch, user?.access_token]);
/**
* Handle Refresh
*
* Purpose: Pull-to-refresh functionality
*/
const handleRefresh = useCallback(() => {
handleFetchPatients();
}, [handleFetchPatients]);
/**
* Handle Search
*
* Purpose: Handle search input changes
*
* @param query - Search query string
*/
const handleSearch = useCallback((query: string) => {
dispatch(setSearchQuery(query));
}, [dispatch]);
/**
* Handle Filter Change
*
* Purpose: Handle filter tab selection
*
* @param filter - Selected filter option
*/
const handleFilterChange = useCallback((filter: 'all' | 'Critical' | 'Routine' | 'Emergency') => {
dispatch(setFilter(filter));
}, [dispatch]);
/**
* Handle Sort Change
*
* Purpose: Handle sort option selection
*
* @param sortOption - Selected sort option
*/
const handleSortChange = useCallback((sortOption: 'date' | 'name' | 'age') => {
dispatch(setSort({ by: sortOption, order: 'desc' }));
setShowSortModal(false);
}, [dispatch]);
/**
* Handle Patient Press
*
* Purpose: Navigate to patient details screen
*
* @param patient - Selected patient
*/
const handlePatientPress = useCallback((patient: MedicalCase) => {
// Helper function to parse JSON strings safely
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(patient.patientdetails);
const patientData = patientDetails.patientdetails || patientDetails;
navigation.navigate('PatientDetails', {
patientId:'1',
patientName: patientData.Name || 'Unknown Patient',
medicalCase: patient,
});
}, [navigation]);
/**
* Handle Emergency Alert
*
* Purpose: Handle emergency alert for critical patients
*
* @param patient - Patient with emergency
*/
const handleEmergencyAlert = useCallback((patient: MedicalCase) => {
// Helper function to parse JSON strings safely
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(patient.patientdetails);
const patientData = patientDetails.patientdetails || patientDetails;
Alert.alert(
'Emergency Alert',
`Critical status for ${patientData.Name || 'Unknown Patient'}\nID: ${patientData.PatID || 'N/A'}`,
[
{
text: 'View Details',
onPress: () => handlePatientPress(patient),
},
{
text: 'Call Physician',
onPress: () => {
// TODO: Implement physician calling functionality
Alert.alert('Calling', `Calling attending physician...`);
},
},
{
text: 'Cancel',
style: 'cancel',
},
]
);
}, [handlePatientPress]);
// ============================================================================
// RENDER HELPERS
// ============================================================================
/**
* Render Patient Item
*
* Purpose: Render individual patient card
*
* @param item - Patient data with render info
*/
const renderPatientItem = ({ item }: { item: MedicalCase }) => (
<PatientCard
patient={item}
onPress={() => handlePatientPress(item)}
onEmergencyPress={() => handleEmergencyAlert(item)}
/>
);
/**
* Render Empty State
*
* Purpose: Render empty state when no patients found
*/
const renderEmptyState = () => {
if (isLoading) return null;
return (
<EmptyState
title={searchQuery ? 'No patients found' : 'No patients available'}
subtitle={
searchQuery
? `No patients match "${searchQuery}"`
: 'Patients will appear here when available'
}
iconName="users"
onRetry={searchQuery ? undefined : handleFetchPatients}
/>
);
};
/**
* Render Loading State
*
* Purpose: Render loading state during initial fetch
*/
if (isLoading && patients.length === 0) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
<LoadingState
title="Loading Patients"
subtitle="Fetching patient data from server..."
/>
</SafeAreaView>
);
}
// ============================================================================
// MAIN RENDER
// ============================================================================
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
{/* Fixed Header */}
<View style={styles.header}>
<View style={styles.headerLeft}>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<Icon name="arrow-left" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
<View>
<Text style={styles.headerTitle}>Patients</Text>
<Text style={styles.headerSubtitle}>Emergency Department</Text>
</View>
</View>
<View style={styles.headerRight}>
<TouchableOpacity
style={styles.headerButton}
onPress={handleRefresh}
disabled={isRefreshing}
>
<Icon
name="refresh-cw"
size={20}
color={isRefreshing ? theme.colors.textMuted : theme.colors.primary}
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.headerButton}
onPress={() => {
// TODO: Implement notifications screen
Alert.alert('Notifications', 'Notifications feature coming soon');
}}
>
<Icon name="bell" size={20} color={theme.colors.textSecondary} />
{/* Notification badge */}
<View style={styles.notificationBadge}>
<Text style={styles.badgeText}>3</Text>
</View>
</TouchableOpacity>
</View>
</View>
{/* Fixed Search and Filter Section */}
<View style={styles.fixedSection}>
{/* Search Bar */}
<View style={styles.searchContainer}>
<SearchBar
value={searchQuery}
onChangeText={handleSearch}
placeholder="Search patients by name, MRN, or room..."
showFilter
onFilterPress={() => setShowSortModal(true)}
/>
</View>
{/* Filter Tabs */}
<View style={styles.filterContainer}>
<FilterTabs
selectedFilter={selectedFilter}
onFilterChange={handleFilterChange}
patientCounts={{
all: patients.length,
Critical: patients.filter((p: MedicalCase) => p.type === 'Critical').length,
Routine: patients.filter((p: MedicalCase) => p.type === 'Routine').length,
Emergency: patients.filter((p: MedicalCase) => p.type === 'Emergency').length,
}}
/>
</View>
{/* Results Summary */}
<View style={styles.resultsSummary}>
<View style={styles.resultsLeft}>
<Icon name="users" size={16} color={theme.colors.textSecondary} />
<Text style={styles.resultsText}>
{filteredPatients.length} patient{filteredPatients.length !== 1 ? 's' : ''} found
</Text>
</View>
<View style={styles.sortInfo}>
<Icon name="filter" size={14} color={theme.colors.textMuted} />
<Text style={styles.sortText}>
Sorted by {sortBy}
</Text>
</View>
</View>
</View>
{/* Scrollable Patient List Only */}
<FlatList
data={filteredPatients}
renderItem={renderPatientItem}
keyExtractor={(item,index) => index.toString()}
ListEmptyComponent={renderEmptyState}
contentContainerStyle={[
styles.listContent,
filteredPatients.length === 0 && styles.emptyListContent
]}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
colors={[theme.colors.primary]}
tintColor={theme.colors.primary}
/>
}
// Performance optimizations
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={10}
initialNumToRender={8}
getItemLayout={(data, index) => ({
length: 120, // Approximate height of PatientCard
offset: 120 * index,
index,
})}
/>
</SafeAreaView>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Header Styles
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.sm,
backgroundColor: theme.colors.background,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
headerLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
backButton: {
marginRight: theme.spacing.sm,
padding: theme.spacing.xs,
},
headerTitle: {
fontSize: 24,
fontWeight: 'bold',
color: theme.colors.textPrimary,
fontFamily: theme.typography.fontFamily.bold,
},
headerSubtitle: {
fontSize: 14,
color: theme.colors.textSecondary,
fontFamily: theme.typography.fontFamily.bold,
},
headerRight: {
flexDirection: 'row',
alignItems: 'center',
},
headerButton: {
padding: theme.spacing.sm,
marginLeft: theme.spacing.xs,
position: 'relative',
},
notificationBadge: {
position: 'absolute',
top: 6,
right: 6,
backgroundColor: theme.colors.error,
borderRadius: 8,
width: 16,
height: 16,
justifyContent: 'center',
alignItems: 'center',
},
badgeText: {
color: theme.colors.background,
fontSize: 10,
fontWeight: 'bold',
},
// Fixed Section Styles
fixedSection: {
paddingHorizontal: theme.spacing.md,
paddingTop: theme.spacing.sm,
paddingBottom: theme.spacing.md,
backgroundColor: theme.colors.background,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
searchContainer: {
marginBottom: theme.spacing.md,
},
filterContainer: {
marginBottom: theme.spacing.sm,
},
// List Styles
listContent: {
paddingTop: theme.spacing.sm,
paddingBottom: theme.spacing.xl,
},
emptyListContent: {
flexGrow: 1,
},
// Results Summary
resultsSummary: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: theme.spacing.sm,
paddingHorizontal: theme.spacing.sm,
backgroundColor: theme.colors.backgroundAlt,
borderRadius: 8,
marginTop: theme.spacing.xs,
},
resultsLeft: {
flexDirection: 'row',
alignItems: 'center',
},
resultsText: {
fontSize: 14,
color: theme.colors.textPrimary,
fontWeight: '500',
marginLeft: theme.spacing.xs,
},
sortInfo: {
flexDirection: 'row',
alignItems: 'center',
},
sortText: {
fontSize: 12,
color: theme.colors.textSecondary,
textTransform: 'capitalize',
marginLeft: theme.spacing.xs,
},
});
export default PatientsScreen;
/*
* End of File: PatientsScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/