618 lines
17 KiB
TypeScript
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.
|
|
*/ |