503 lines
14 KiB
TypeScript
503 lines
14 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, useCallback } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
TouchableOpacity,
|
|
FlatList,
|
|
RefreshControl,
|
|
SafeAreaView,
|
|
StyleSheet,
|
|
Alert,
|
|
} from 'react-native';
|
|
import { useNavigation } from '@react-navigation/native';
|
|
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
|
import { theme } from '../../../theme/theme';
|
|
|
|
// Components
|
|
import PatientCard from '../components/PatientCard';
|
|
import SearchBar from '../components/SearchBar';
|
|
import FilterTabs from '../components/FilterTabs';
|
|
import LoadingState from '../components/LoadingState';
|
|
import EmptyState from '../components/EmptyState';
|
|
|
|
// Redux
|
|
import {
|
|
fetchPatients,
|
|
setSearchQuery,
|
|
setFilter,
|
|
} from '../redux/patientCareSlice';
|
|
import {
|
|
selectPatients,
|
|
selectFilteredPatients,
|
|
selectPatientsLoading,
|
|
selectIsRefreshing,
|
|
selectPatientsError,
|
|
selectSearchQuery,
|
|
selectSelectedFilter,
|
|
selectPatientCounts,
|
|
} from '../redux/patientCareSelectors';
|
|
|
|
// Types
|
|
import { PatientData } from '../redux/patientCareSlice';
|
|
import { selectUser } from '../../Auth/redux/authSelectors';
|
|
|
|
// ============================================================================
|
|
// 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, Processed, Pending)
|
|
* - Sort options (Date, Name, Processed)
|
|
* - 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 = () => {
|
|
// ============================================================================
|
|
// STATE MANAGEMENT
|
|
// ============================================================================
|
|
|
|
const dispatch = useAppDispatch();
|
|
const navigation = useNavigation();
|
|
|
|
// 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 patientCounts = useAppSelector(selectPatientCounts);
|
|
|
|
// Auth state
|
|
const user = useAppSelector(selectUser);
|
|
|
|
// ============================================================================
|
|
// EFFECTS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Fetch Patients on Mount
|
|
*
|
|
* Purpose: Load patients when component mounts
|
|
*/
|
|
useEffect(() => {
|
|
if (user?.access_token) {
|
|
dispatch(fetchPatients(user.access_token));
|
|
}
|
|
}, [dispatch, user?.access_token]);
|
|
|
|
/**
|
|
* Clear Error on Unmount
|
|
*
|
|
* Purpose: Clean up error state when component unmounts
|
|
*/
|
|
useEffect(() => {
|
|
return () => {
|
|
// No clearError action in this file, so this effect is removed.
|
|
};
|
|
}, [dispatch]);
|
|
|
|
// ============================================================================
|
|
// EVENT HANDLERS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Handle Refresh
|
|
*
|
|
* Purpose: Handle pull-to-refresh functionality
|
|
*/
|
|
const handleRefresh = useCallback(() => {
|
|
if (user?.access_token) {
|
|
dispatch(fetchPatients(user.access_token));
|
|
}
|
|
}, [dispatch, user?.access_token]);
|
|
|
|
/**
|
|
* Handle Search
|
|
*
|
|
* Purpose: Handle search query changes
|
|
*
|
|
* @param query - Search query string
|
|
*/
|
|
const handleSearch = useCallback((query: string) => {
|
|
dispatch(setSearchQuery(query));
|
|
}, [dispatch]);
|
|
|
|
/**
|
|
* Handle Filter Change
|
|
*
|
|
* Purpose: Update the selected filter and refresh the list
|
|
*/
|
|
const handleFilterChange = useCallback((filter: 'all' | 'processed' | 'pending') => {
|
|
dispatch(setFilter(filter));
|
|
}, [dispatch]);
|
|
|
|
/**
|
|
* Handle Patient Press
|
|
*
|
|
* Purpose: Navigate to patient details when a patient card is pressed
|
|
*/
|
|
const handlePatientPress = useCallback((patient: PatientData) => {
|
|
(navigation as any).navigate('PatientDetails', {
|
|
patientId: patient.patid,
|
|
patientName: patient.patient_info.name,
|
|
});
|
|
}, [navigation]);
|
|
|
|
/**
|
|
* Handle Emergency Alert
|
|
*
|
|
* Purpose: Show emergency alert for critical patients
|
|
*/
|
|
const handleEmergencyAlert = useCallback((patient: PatientData) => {
|
|
Alert.alert(
|
|
'Emergency Alert',
|
|
`Patient ${patient.patient_info.name} (ID: ${patient.patid}) requires immediate attention!\n\nStatus: ${patient.patient_info.report_status}\nPriority: ${patient.patient_info.status}`,
|
|
[
|
|
{ text: 'Cancel', style: 'cancel' },
|
|
{ text: 'View Details', onPress: () => handlePatientPress(patient) },
|
|
]
|
|
);
|
|
}, [handlePatientPress]);
|
|
|
|
// ============================================================================
|
|
// RENDER HELPERS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Render Patient Card
|
|
*
|
|
* Purpose: Render individual patient card component
|
|
*/
|
|
const renderPatientCard = useCallback(({ item }: { item: PatientData }) => (
|
|
<PatientCard
|
|
patient={item}
|
|
onPress={() => handlePatientPress(item)}
|
|
onEmergencyPress={() => handleEmergencyAlert(item)}
|
|
/>
|
|
), [handlePatientPress, handleEmergencyAlert]);
|
|
|
|
/**
|
|
* Render Header
|
|
*
|
|
* Purpose: Render the screen header with title and action buttons
|
|
*/
|
|
const renderHeader = () => (
|
|
<View style={styles.header}>
|
|
<View style={styles.headerLeft}>
|
|
<Text style={styles.headerTitle}>Patients</Text>
|
|
<Text style={styles.headerSubtitle}>
|
|
{filteredPatients.length} of {patients?.length || 0} patients
|
|
</Text>
|
|
</View>
|
|
|
|
{/* <View style={styles.headerRight}>
|
|
<TouchableOpacity
|
|
style={styles.actionButton}
|
|
onPress={() => {
|
|
// TODO: Implement sort modal
|
|
}}
|
|
>
|
|
<Text style={styles.actionButtonText}>Sort</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={styles.actionButton}
|
|
onPress={() => {
|
|
// TODO: Implement filter modal
|
|
}}
|
|
>
|
|
<Text style={styles.actionButtonText}>Filter</Text>
|
|
</TouchableOpacity>
|
|
</View> */}
|
|
</View>
|
|
);
|
|
|
|
/**
|
|
* Render Empty State
|
|
*
|
|
* Purpose: Render empty state when no patients found
|
|
*/
|
|
const renderEmptyState = () => (
|
|
<EmptyState
|
|
title="No Patients Found"
|
|
subtitle={searchQuery.trim() ?
|
|
`No patients match "${searchQuery}"` :
|
|
"No patients available at the moment"
|
|
}
|
|
iconName="users"
|
|
onRetry={handleRefresh}
|
|
retryText="Refresh"
|
|
/>
|
|
);
|
|
|
|
// ============================================================================
|
|
// MAIN RENDER
|
|
// ============================================================================
|
|
|
|
if (error && !isLoading) {
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
<View style={styles.errorContainer}>
|
|
<Text style={styles.errorTitle}>Error Loading Patients</Text>
|
|
<Text style={styles.errorMessage}>{error}</Text>
|
|
<TouchableOpacity
|
|
style={styles.retryButton}
|
|
onPress={handleRefresh}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Text style={styles.retryButtonText}>Retry</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
|
|
{/* Header */}
|
|
{renderHeader()}
|
|
|
|
{/* Search and Filters */}
|
|
<View style={styles.searchAndFilters}>
|
|
<SearchBar
|
|
value={searchQuery}
|
|
onChangeText={handleSearch}
|
|
placeholder="Search patients, ID, institution..."
|
|
/>
|
|
|
|
<FilterTabs
|
|
selectedFilter={selectedFilter}
|
|
onFilterChange={handleFilterChange}
|
|
patientCounts={patientCounts}
|
|
/>
|
|
</View>
|
|
|
|
{/* Loading State */}
|
|
{isLoading && patients.length === 0 && (
|
|
<View style={styles.centerContainer}>
|
|
<LoadingState />
|
|
</View>
|
|
)}
|
|
|
|
{/* Error State */}
|
|
{error && patients.length === 0 && (
|
|
<View style={styles.centerContainer}>
|
|
<EmptyState
|
|
iconName="alert-circle"
|
|
title="Error Loading Patients"
|
|
subtitle={error}
|
|
retryText="Retry"
|
|
onRetry={handleRefresh}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{/* Empty State */}
|
|
{!isLoading && !error && patients.length === 0 && (
|
|
<View style={styles.centerContainer}>
|
|
<EmptyState
|
|
iconName="users"
|
|
title="No Patients Found"
|
|
subtitle="There are no patients in the system yet."
|
|
retryText="Refresh"
|
|
onRetry={handleRefresh}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{/* No Results State */}
|
|
{!isLoading && !error && patients.length > 0 && filteredPatients.length === 0 && (
|
|
<View style={styles.centerContainer}>
|
|
<EmptyState
|
|
iconName="search"
|
|
title="No Results Found"
|
|
subtitle={`No patients match your search "${searchQuery}" and filter "${selectedFilter}"`}
|
|
retryText="Clear Search"
|
|
onRetry={() => {
|
|
handleSearch('');
|
|
handleFilterChange('all');
|
|
}}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{/* Patient List */}
|
|
{!isLoading && !error && filteredPatients.length > 0 && (
|
|
<FlatList
|
|
data={filteredPatients}
|
|
renderItem={renderPatientCard}
|
|
keyExtractor={(item) => item.patid}
|
|
contentContainerStyle={styles.listContainer}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={isRefreshing}
|
|
onRefresh={handleRefresh}
|
|
colors={[theme.colors.primary]}
|
|
tintColor={theme.colors.primary}
|
|
/>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{/* TODO: Implement sort and filter modals for enhanced functionality */}
|
|
|
|
{/* Note: Patient data will be loaded from API when fetchPatients is called */}
|
|
{/* Currently using mock data from Redux slice for development */}
|
|
</SafeAreaView>
|
|
);
|
|
};
|
|
|
|
// ============================================================================
|
|
// STYLES
|
|
// ============================================================================
|
|
|
|
const styles = StyleSheet.create({
|
|
// Container Styles
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: theme.colors.background,
|
|
},
|
|
|
|
// Header Styles
|
|
header: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingVertical: theme.spacing.md,
|
|
backgroundColor: theme.colors.background,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: theme.colors.border,
|
|
marginBottom: theme.spacing.md,
|
|
},
|
|
headerLeft: {
|
|
flex: 1,
|
|
},
|
|
headerRight: {
|
|
flexDirection: 'row',
|
|
gap: theme.spacing.sm,
|
|
},
|
|
headerTitle: {
|
|
fontSize: 24,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
headerSubtitle: {
|
|
fontSize: 14,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
},
|
|
actionButton: {
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingVertical: theme.spacing.sm,
|
|
borderRadius: 8,
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
},
|
|
actionButtonText: {
|
|
color: theme.colors.textSecondary,
|
|
fontSize: 14,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
},
|
|
|
|
// Search and Filters
|
|
searchAndFilters: {
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingBottom: theme.spacing.sm,
|
|
backgroundColor: theme.colors.background,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: theme.colors.border,
|
|
},
|
|
|
|
// Center Container for States
|
|
centerContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
padding: theme.spacing.md,
|
|
},
|
|
|
|
// List Styles
|
|
listContainer: {
|
|
paddingBottom: theme.spacing.lg,
|
|
},
|
|
listFooter: {
|
|
paddingVertical: theme.spacing.md,
|
|
alignItems: 'center',
|
|
},
|
|
footerText: {
|
|
fontSize: 14,
|
|
color: theme.colors.textMuted,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
},
|
|
|
|
// Error State Styles
|
|
errorContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
padding: theme.spacing.xl,
|
|
},
|
|
errorTitle: {
|
|
fontSize: 20,
|
|
color: theme.colors.error,
|
|
marginBottom: theme.spacing.sm,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
textAlign: 'center',
|
|
},
|
|
errorMessage: {
|
|
fontSize: 16,
|
|
color: theme.colors.textSecondary,
|
|
marginBottom: theme.spacing.lg,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
textAlign: 'center',
|
|
},
|
|
retryButton: {
|
|
backgroundColor: theme.colors.primary,
|
|
paddingHorizontal: theme.spacing.lg,
|
|
paddingVertical: theme.spacing.md,
|
|
borderRadius: 8,
|
|
minWidth: 120,
|
|
alignItems: 'center',
|
|
},
|
|
retryButtonText: {
|
|
color: theme.colors.background,
|
|
fontSize: 16,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
},
|
|
});
|
|
|
|
export default PatientsScreen;
|
|
|
|
/*
|
|
* End of File: PatientsScreen.tsx
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/ |