patient data fetched to patient tab
This commit is contained in:
parent
9eb1416866
commit
84b63e401f
161
app/modules/PatientCare/components/EmptyState.tsx
Normal file
161
app/modules/PatientCare/components/EmptyState.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
/*
|
||||
* File: EmptyState.tsx
|
||||
* Description: Empty state component for when no patients are found
|
||||
* 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';
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
iconName?: string;
|
||||
onRetry?: () => void;
|
||||
retryText?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EMPTY STATE COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* EmptyState Component
|
||||
*
|
||||
* Purpose: Display empty state when no patients are found
|
||||
*
|
||||
* Features:
|
||||
* - Customizable title and subtitle
|
||||
* - Icon display
|
||||
* - Optional retry functionality
|
||||
* - Centered layout with proper spacing
|
||||
* - Medical-themed design
|
||||
*/
|
||||
const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
title,
|
||||
subtitle,
|
||||
iconName = 'users',
|
||||
onRetry,
|
||||
retryText = 'Retry',
|
||||
}) => {
|
||||
// ============================================================================
|
||||
// MAIN RENDER
|
||||
// ============================================================================
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* Icon */}
|
||||
<View style={styles.iconContainer}>
|
||||
<Icon
|
||||
name={iconName}
|
||||
size={64}
|
||||
color={theme.colors.textMuted}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Title */}
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
|
||||
{/* Subtitle */}
|
||||
<Text style={styles.subtitle}>{subtitle}</Text>
|
||||
|
||||
{/* Retry Button */}
|
||||
{onRetry && (
|
||||
<TouchableOpacity
|
||||
style={styles.retryButton}
|
||||
onPress={onRetry}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Icon
|
||||
name="refresh-cw"
|
||||
size={16}
|
||||
color={theme.colors.background}
|
||||
style={styles.retryIcon}
|
||||
/>
|
||||
<Text style={styles.retryText}>{retryText}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
// ============================================================================
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: theme.spacing.xl,
|
||||
paddingVertical: theme.spacing.xxl,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 60,
|
||||
backgroundColor: theme.colors.backgroundAlt,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing.lg,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
color: theme.colors.textPrimary,
|
||||
fontFamily: theme.typography.fontFamily.primary,
|
||||
textAlign: 'center',
|
||||
marginBottom: theme.spacing.sm,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: theme.colors.textSecondary,
|
||||
fontFamily: theme.typography.fontFamily.primary,
|
||||
textAlign: 'center',
|
||||
lineHeight: 24,
|
||||
marginBottom: theme.spacing.lg,
|
||||
},
|
||||
retryButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.colors.primary,
|
||||
paddingHorizontal: theme.spacing.lg,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
borderRadius: 12,
|
||||
shadowColor: theme.colors.primary,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
retryIcon: {
|
||||
marginRight: theme.spacing.xs,
|
||||
},
|
||||
retryText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: theme.colors.background,
|
||||
fontFamily: theme.typography.fontFamily.primary,
|
||||
},
|
||||
});
|
||||
|
||||
export default EmptyState;
|
||||
|
||||
/*
|
||||
* End of File: EmptyState.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
338
app/modules/PatientCare/components/FilterTabs.tsx
Normal file
338
app/modules/PatientCare/components/FilterTabs.tsx
Normal file
@ -0,0 +1,338 @@
|
||||
/*
|
||||
* File: FilterTabs.tsx
|
||||
* Description: Filter tabs component for patient status filtering
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
} from 'react-native';
|
||||
import { theme } from '../../../theme/theme';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
interface FilterTabsProps {
|
||||
selectedFilter: 'all' | 'Critical' | 'Routine' | 'Emergency';
|
||||
onFilterChange: (filter: 'all' | 'Critical' | 'Routine' | 'Emergency') => void;
|
||||
patientCounts: {
|
||||
all: number;
|
||||
Critical: number;
|
||||
Routine: number;
|
||||
Emergency: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface FilterTab {
|
||||
id: 'all' | 'Critical' | 'Routine' | 'Emergency';
|
||||
label: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
activeColor: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FILTER TABS COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* FilterTabs Component
|
||||
*
|
||||
* Purpose: Provide filtering options for patient list
|
||||
*
|
||||
* Features:
|
||||
* - Multiple filter options (All, Active, Critical, Discharged)
|
||||
* - Patient count display for each filter
|
||||
* - Visual indicators with icons and colors
|
||||
* - Horizontal scrollable layout
|
||||
* - Active state highlighting
|
||||
* - ER-focused design with medical priority colors
|
||||
*/
|
||||
const FilterTabs: React.FC<FilterTabsProps> = ({
|
||||
selectedFilter,
|
||||
onFilterChange,
|
||||
patientCounts,
|
||||
}) => {
|
||||
// ============================================================================
|
||||
// TAB CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
const filterTabs: FilterTab[] = [
|
||||
{
|
||||
id: 'all',
|
||||
label: 'All Cases',
|
||||
icon: 'users',
|
||||
color: theme.colors.textSecondary,
|
||||
activeColor: theme.colors.primary,
|
||||
},
|
||||
{
|
||||
id: 'Critical',
|
||||
label: 'Critical',
|
||||
icon: 'alert-triangle',
|
||||
color: theme.colors.error,
|
||||
activeColor: theme.colors.error,
|
||||
},
|
||||
{
|
||||
id: 'Emergency',
|
||||
label: 'Emergency',
|
||||
icon: 'alert-circle',
|
||||
color: '#FF8C00',
|
||||
activeColor: '#FF8C00',
|
||||
},
|
||||
{
|
||||
id: 'Routine',
|
||||
label: 'Routine',
|
||||
icon: 'check-circle',
|
||||
color: theme.colors.success,
|
||||
activeColor: theme.colors.success,
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get Patient Count for Filter
|
||||
*
|
||||
* Purpose: Get the count of patients for a specific filter
|
||||
*
|
||||
* @param filterId - Filter ID
|
||||
* @returns Number of patients for the filter
|
||||
*/
|
||||
const getPatientCount = (filterId: string): number => {
|
||||
switch (filterId) {
|
||||
case 'all':
|
||||
return patientCounts.all;
|
||||
case 'Critical':
|
||||
return patientCounts.Critical;
|
||||
case 'Emergency':
|
||||
return patientCounts.Emergency;
|
||||
case 'Routine':
|
||||
return patientCounts.Routine;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// RENDER HELPERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Render Filter Tab
|
||||
*
|
||||
* Purpose: Render individual filter tab
|
||||
*
|
||||
* @param tab - Filter tab configuration
|
||||
*/
|
||||
const renderFilterTab = (tab: FilterTab) => {
|
||||
const isSelected = selectedFilter === tab.id;
|
||||
const patientCount = getPatientCount(tab.id);
|
||||
const tabColor = isSelected ? tab.activeColor : tab.color;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={tab.id}
|
||||
style={[
|
||||
styles.tab,
|
||||
isSelected && styles.tabSelected,
|
||||
isSelected && { borderBottomColor: tab.activeColor },
|
||||
]}
|
||||
onPress={() => onFilterChange(tab.id)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{/* Tab Icon */}
|
||||
<Icon
|
||||
name={tab.icon}
|
||||
size={16}
|
||||
color={tabColor}
|
||||
style={styles.tabIcon}
|
||||
/>
|
||||
|
||||
{/* Tab Content */}
|
||||
<View style={styles.tabContent}>
|
||||
<Text
|
||||
style={[
|
||||
styles.tabLabel,
|
||||
isSelected && styles.tabLabelSelected,
|
||||
{ color: tabColor },
|
||||
]}
|
||||
>
|
||||
{tab.label}
|
||||
</Text>
|
||||
|
||||
{/* Patient Count Badge */}
|
||||
<View
|
||||
style={[
|
||||
styles.countBadge,
|
||||
isSelected && styles.countBadgeSelected,
|
||||
{ backgroundColor: isSelected ? tab.activeColor : theme.colors.backgroundAlt },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.countText,
|
||||
isSelected && styles.countTextSelected,
|
||||
{ color: isSelected ? theme.colors.background : tabColor },
|
||||
]}
|
||||
>
|
||||
{patientCount}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Critical Indicator */}
|
||||
{tab.id === 'Critical' && patientCount > 0 && (
|
||||
<View style={styles.criticalIndicator}>
|
||||
<View style={styles.pulseDot} />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// MAIN RENDER
|
||||
// ============================================================================
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
{filterTabs.map(renderFilterTab)}
|
||||
</ScrollView>
|
||||
|
||||
{/* Active Filter Indicator */}
|
||||
<View style={styles.activeIndicator}>
|
||||
<View style={styles.indicatorLine} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
// ============================================================================
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: theme.spacing.sm,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: theme.spacing.xs,
|
||||
},
|
||||
|
||||
// Tab Styles
|
||||
tab: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
marginHorizontal: theme.spacing.xs,
|
||||
borderRadius: 12,
|
||||
backgroundColor: theme.colors.backgroundAlt,
|
||||
borderBottomWidth: 3,
|
||||
borderBottomColor: 'transparent',
|
||||
position: 'relative',
|
||||
minWidth: 100,
|
||||
},
|
||||
tabSelected: {
|
||||
backgroundColor: theme.colors.background,
|
||||
shadowColor: theme.colors.shadow,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
tabIcon: {
|
||||
marginRight: theme.spacing.xs,
|
||||
},
|
||||
tabContent: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
tabLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
fontFamily: theme.typography.fontFamily.primary,
|
||||
flex: 1,
|
||||
},
|
||||
tabLabelSelected: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
|
||||
// Count Badge Styles
|
||||
countBadge: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 12,
|
||||
marginLeft: theme.spacing.xs,
|
||||
minWidth: 24,
|
||||
alignItems: 'center',
|
||||
},
|
||||
countBadgeSelected: {
|
||||
shadowColor: theme.colors.shadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
countText: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
countTextSelected: {
|
||||
color: theme.colors.background,
|
||||
},
|
||||
|
||||
// Critical Indicator
|
||||
criticalIndicator: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
width: 8,
|
||||
height: 8,
|
||||
},
|
||||
pulseDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.colors.error,
|
||||
// Note: In a real app, you'd add animation here
|
||||
},
|
||||
|
||||
// Active Filter Indicator
|
||||
activeIndicator: {
|
||||
marginTop: theme.spacing.xs,
|
||||
alignItems: 'center',
|
||||
},
|
||||
indicatorLine: {
|
||||
width: 40,
|
||||
height: 2,
|
||||
backgroundColor: theme.colors.primary,
|
||||
borderRadius: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default FilterTabs;
|
||||
|
||||
/*
|
||||
* End of File: FilterTabs.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
179
app/modules/PatientCare/components/LoadingState.tsx
Normal file
179
app/modules/PatientCare/components/LoadingState.tsx
Normal file
@ -0,0 +1,179 @@
|
||||
/*
|
||||
* File: LoadingState.tsx
|
||||
* Description: Loading state component for patient data fetching
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import { theme } from '../../../theme/theme';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
interface LoadingStateProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
showIcon?: boolean;
|
||||
iconName?: string;
|
||||
size?: 'small' | 'large';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LOADING STATE COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* LoadingState Component
|
||||
*
|
||||
* Purpose: Display loading state during data fetching
|
||||
*
|
||||
* Features:
|
||||
* - Customizable loading messages
|
||||
* - Optional icon display
|
||||
* - Different sizes (small/large)
|
||||
* - Centered layout with spinner
|
||||
* - Medical-themed design
|
||||
*/
|
||||
const LoadingState: React.FC<LoadingStateProps> = ({
|
||||
title = 'Loading...',
|
||||
subtitle = 'Please wait while we fetch the data',
|
||||
showIcon = true,
|
||||
iconName = 'loader',
|
||||
size = 'large',
|
||||
}) => {
|
||||
// ============================================================================
|
||||
// RENDER HELPERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Render Large Loading State
|
||||
*
|
||||
* Purpose: Render full-screen loading state
|
||||
*/
|
||||
const renderLargeState = () => (
|
||||
<View style={styles.container}>
|
||||
{/* Loading Animation */}
|
||||
<View style={styles.loadingContainer}>
|
||||
{showIcon && (
|
||||
<View style={styles.iconContainer}>
|
||||
<Icon
|
||||
name={iconName}
|
||||
size={32}
|
||||
color={theme.colors.primary}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<ActivityIndicator
|
||||
size="large"
|
||||
color={theme.colors.primary}
|
||||
style={styles.spinner}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Loading Text */}
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<Text style={styles.subtitle}>{subtitle}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
/**
|
||||
* Render Small Loading State
|
||||
*
|
||||
* Purpose: Render compact loading state
|
||||
*/
|
||||
const renderSmallState = () => (
|
||||
<View style={styles.smallContainer}>
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={theme.colors.primary}
|
||||
style={styles.smallSpinner}
|
||||
/>
|
||||
<Text style={styles.smallText}>{title}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// MAIN RENDER
|
||||
// ============================================================================
|
||||
|
||||
return size === 'large' ? renderLargeState() : renderSmallState();
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
// ============================================================================
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
// Large Loading State
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: theme.spacing.xl,
|
||||
paddingVertical: theme.spacing.xxl,
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
loadingContainer: {
|
||||
position: 'relative',
|
||||
marginBottom: theme.spacing.lg,
|
||||
},
|
||||
iconContainer: {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
marginTop: -16,
|
||||
marginLeft: -16,
|
||||
zIndex: 1,
|
||||
},
|
||||
spinner: {
|
||||
transform: [{ scale: 1.5 }],
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
color: theme.colors.textPrimary,
|
||||
fontFamily: theme.typography.fontFamily.primary,
|
||||
textAlign: 'center',
|
||||
marginBottom: theme.spacing.sm,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: theme.colors.textSecondary,
|
||||
fontFamily: theme.typography.fontFamily.primary,
|
||||
textAlign: 'center',
|
||||
lineHeight: 24,
|
||||
},
|
||||
|
||||
// Small Loading State
|
||||
smallContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: theme.spacing.md,
|
||||
},
|
||||
smallSpinner: {
|
||||
marginRight: theme.spacing.sm,
|
||||
},
|
||||
smallText: {
|
||||
fontSize: 14,
|
||||
color: theme.colors.textSecondary,
|
||||
fontFamily: theme.typography.fontFamily.primary,
|
||||
},
|
||||
});
|
||||
|
||||
export default LoadingState;
|
||||
|
||||
/*
|
||||
* End of File: LoadingState.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
485
app/modules/PatientCare/components/PatientCard.tsx
Normal file
485
app/modules/PatientCare/components/PatientCard.tsx
Normal file
@ -0,0 +1,485 @@
|
||||
/*
|
||||
* 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(patient.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.
|
||||
*/
|
||||
178
app/modules/PatientCare/components/SearchBar.tsx
Normal file
178
app/modules/PatientCare/components/SearchBar.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
/*
|
||||
* File: SearchBar.tsx
|
||||
* Description: Search bar component for patient filtering
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
View,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import { theme } from '../../../theme/theme';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
interface SearchBarProps {
|
||||
value: string;
|
||||
onChangeText: (text: string) => void;
|
||||
placeholder?: string;
|
||||
showFilter?: boolean;
|
||||
onFilterPress?: () => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SEARCH BAR COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* SearchBar Component
|
||||
*
|
||||
* Purpose: Provide search functionality for patient list
|
||||
*
|
||||
* Features:
|
||||
* - Real-time search input
|
||||
* - Clear button when text is present
|
||||
* - Optional filter button
|
||||
* - Modern design with icons
|
||||
* - Optimized for medical data search
|
||||
*/
|
||||
const SearchBar: React.FC<SearchBarProps> = ({
|
||||
value,
|
||||
onChangeText,
|
||||
placeholder = 'Search patients...',
|
||||
showFilter = false,
|
||||
onFilterPress,
|
||||
}) => {
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Handle Clear Search
|
||||
*
|
||||
* Purpose: Clear the search input
|
||||
*/
|
||||
const handleClear = () => {
|
||||
onChangeText('');
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// MAIN RENDER
|
||||
// ============================================================================
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.searchContainer}>
|
||||
{/* Search Icon */}
|
||||
<Icon
|
||||
name="search"
|
||||
size={18}
|
||||
color={theme.colors.textMuted}
|
||||
style={styles.searchIcon}
|
||||
/>
|
||||
|
||||
{/* Search Input */}
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
returnKeyType="search"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
|
||||
{/* Clear Button */}
|
||||
{value.length > 0 && (
|
||||
<TouchableOpacity
|
||||
style={styles.clearButton}
|
||||
onPress={handleClear}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Icon
|
||||
name="x-circle"
|
||||
size={18}
|
||||
color={theme.colors.textMuted}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Filter Button */}
|
||||
{showFilter && (
|
||||
<TouchableOpacity
|
||||
style={styles.filterButton}
|
||||
onPress={onFilterPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Icon
|
||||
name="sliders"
|
||||
size={18}
|
||||
color={theme.colors.primary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
// ============================================================================
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing.sm,
|
||||
},
|
||||
searchContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.colors.backgroundAlt,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: theme.spacing.sm,
|
||||
paddingVertical: theme.spacing.xs,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
},
|
||||
searchIcon: {
|
||||
marginRight: theme.spacing.xs,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
color: theme.colors.textPrimary,
|
||||
fontFamily: theme.typography.fontFamily.primary,
|
||||
paddingVertical: theme.spacing.xs,
|
||||
},
|
||||
clearButton: {
|
||||
padding: theme.spacing.xs,
|
||||
marginLeft: theme.spacing.xs,
|
||||
},
|
||||
filterButton: {
|
||||
backgroundColor: theme.colors.backgroundAlt,
|
||||
borderRadius: 12,
|
||||
padding: theme.spacing.sm,
|
||||
marginLeft: theme.spacing.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.primary,
|
||||
},
|
||||
});
|
||||
|
||||
export default SearchBar;
|
||||
|
||||
/*
|
||||
* End of File: SearchBar.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
18
app/modules/PatientCare/components/index.ts
Normal file
18
app/modules/PatientCare/components/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* File: index.ts
|
||||
* Description: Barrel export for PatientCare components
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
export { default as PatientCard } from './PatientCard';
|
||||
export { default as SearchBar } from './SearchBar';
|
||||
export { default as FilterTabs } from './FilterTabs';
|
||||
export { default as EmptyState } from './EmptyState';
|
||||
export { default as LoadingState } from './LoadingState';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
25
app/modules/PatientCare/index.ts
Normal file
25
app/modules/PatientCare/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* File: index.ts
|
||||
* Description: Barrel export for PatientCare module
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
// Screens
|
||||
export * from './screens';
|
||||
|
||||
// Components
|
||||
export * from './components';
|
||||
|
||||
// Services
|
||||
export * from './services';
|
||||
|
||||
// Redux
|
||||
export * from './redux/patientCareSlice';
|
||||
export * from './redux/patientCareSelectors';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
388
app/modules/PatientCare/redux/patientCareSelectors.ts
Normal file
388
app/modules/PatientCare/redux/patientCareSelectors.ts
Normal file
@ -0,0 +1,388 @@
|
||||
/*
|
||||
* File: patientCareSelectors.ts
|
||||
* Description: Redux selectors for patient care state
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from '../../../store/store';
|
||||
import { MedicalCase } from '../../../shared/types';
|
||||
|
||||
// ============================================================================
|
||||
// BASE SELECTORS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Select Patient Care State
|
||||
*
|
||||
* Purpose: Get the entire patient care state
|
||||
*/
|
||||
export const selectPatientCareState = (state: RootState) => state.patientCare;
|
||||
|
||||
/**
|
||||
* Select Patients
|
||||
*
|
||||
* Purpose: Get the patients array
|
||||
*/
|
||||
export const selectPatients = (state: RootState) => state.patientCare.patients;
|
||||
|
||||
/**
|
||||
* Select Current Patient
|
||||
*
|
||||
* Purpose: Get the currently selected patient
|
||||
*/
|
||||
export const selectCurrentPatient = (state: RootState) => state.patientCare.currentPatient;
|
||||
|
||||
/**
|
||||
* Select Patients Loading State
|
||||
*
|
||||
* Purpose: Get the loading state for patients
|
||||
*/
|
||||
export const selectPatientsLoading = (state: RootState) => state.patientCare.isLoading;
|
||||
|
||||
/**
|
||||
* Select Is Refreshing State
|
||||
*
|
||||
* Purpose: Get the refreshing state for pull-to-refresh
|
||||
*/
|
||||
export const selectIsRefreshing = (state: RootState) => state.patientCare.isRefreshing;
|
||||
|
||||
/**
|
||||
* Select Patient Details Loading State
|
||||
*
|
||||
* Purpose: Get the loading state for patient details
|
||||
*/
|
||||
export const selectPatientDetailsLoading = (state: RootState) => state.patientCare.isLoadingPatientDetails;
|
||||
|
||||
/**
|
||||
* Select Patients Error
|
||||
*
|
||||
* Purpose: Get the error state for patients
|
||||
*/
|
||||
export const selectPatientsError = (state: RootState) => state.patientCare.error;
|
||||
|
||||
/**
|
||||
* Select Search Query
|
||||
*
|
||||
* Purpose: Get the current search query
|
||||
*/
|
||||
export const selectSearchQuery = (state: RootState) => state.patientCare.searchQuery;
|
||||
|
||||
/**
|
||||
* Select Selected Filter
|
||||
*
|
||||
* Purpose: Get the currently selected filter
|
||||
*/
|
||||
export const selectSelectedFilter = (state: RootState) => state.patientCare.selectedFilter;
|
||||
|
||||
/**
|
||||
* Select Sort By
|
||||
*
|
||||
* Purpose: Get the current sort option
|
||||
*/
|
||||
export const selectSortBy = (state: RootState) => state.patientCare.sortBy;
|
||||
|
||||
/**
|
||||
* Select Sort Order
|
||||
*
|
||||
* Purpose: Get the current sort order
|
||||
*/
|
||||
export const selectSortOrder = (state: RootState) => state.patientCare.sortOrder;
|
||||
|
||||
/**
|
||||
* Select Pagination Info
|
||||
*
|
||||
* Purpose: Get pagination-related state
|
||||
*/
|
||||
export const selectPaginationInfo = (state: RootState) => ({
|
||||
currentPage: state.patientCare.currentPage,
|
||||
itemsPerPage: state.patientCare.itemsPerPage,
|
||||
totalItems: state.patientCare.totalItems,
|
||||
});
|
||||
|
||||
/**
|
||||
* Select Last Updated
|
||||
*
|
||||
* Purpose: Get the last updated timestamp
|
||||
*/
|
||||
export const selectLastUpdated = (state: RootState) => state.patientCare.lastUpdated;
|
||||
|
||||
// ============================================================================
|
||||
// COMPUTED SELECTORS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Select Filtered Patients
|
||||
*
|
||||
* Purpose: Get patients filtered by search query and selected filter
|
||||
*/
|
||||
export const selectFilteredPatients = createSelector(
|
||||
[selectPatients, selectSearchQuery, selectSelectedFilter, selectSortBy, selectSortOrder],
|
||||
(patients, searchQuery, selectedFilter, sortBy, sortOrder) => {
|
||||
let filteredPatients = [...patients];
|
||||
|
||||
// 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 {};
|
||||
};
|
||||
|
||||
// Apply filter
|
||||
if (selectedFilter !== 'all') {
|
||||
filteredPatients = filteredPatients.filter(
|
||||
patient => patient.type === selectedFilter
|
||||
);
|
||||
}
|
||||
|
||||
// Apply search
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase().trim();
|
||||
filteredPatients = filteredPatients.filter(patient => {
|
||||
const patientDetails = parseJsonSafely(patient.patientdetails);
|
||||
const patientData = patientDetails.patientdetails || patientDetails;
|
||||
|
||||
const name = (patientData.Name || '').toLowerCase();
|
||||
const patId = (patientData.PatID || '').toLowerCase();
|
||||
const instName = (patientData.InstName || '').toLowerCase();
|
||||
const modality = (patientData.Modality || '').toLowerCase();
|
||||
|
||||
return (
|
||||
name.includes(query) ||
|
||||
patId.includes(query) ||
|
||||
instName.includes(query) ||
|
||||
modality.includes(query)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
filteredPatients.sort((a, b) => {
|
||||
const patientDetailsA = parseJsonSafely(a.patientdetails);
|
||||
const patientDataA = patientDetailsA.patientdetails || patientDetailsA;
|
||||
const patientDetailsB = parseJsonSafely(b.patientdetails);
|
||||
const patientDataB = patientDetailsB.patientdetails || patientDetailsB;
|
||||
|
||||
let aValue: any;
|
||||
let bValue: any;
|
||||
|
||||
switch (sortBy) {
|
||||
case 'name':
|
||||
aValue = (patientDataA.Name || '').toLowerCase();
|
||||
bValue = (patientDataB.Name || '').toLowerCase();
|
||||
break;
|
||||
case 'age':
|
||||
aValue = parseInt(patientDataA.PatAge || '0');
|
||||
bValue = parseInt(patientDataB.PatAge || '0');
|
||||
break;
|
||||
case 'date':
|
||||
default:
|
||||
aValue = new Date(a.created_at).getTime();
|
||||
bValue = new Date(b.created_at).getTime();
|
||||
break;
|
||||
}
|
||||
|
||||
if (aValue < bValue) {
|
||||
return sortOrder === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (aValue > bValue) {
|
||||
return sortOrder === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return filteredPatients;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Critical Patients
|
||||
*
|
||||
* Purpose: Get patients with critical priority
|
||||
*/
|
||||
export const selectCriticalPatients = createSelector(
|
||||
[selectPatients],
|
||||
(patients) => patients.filter(patient => patient.type === 'Critical')
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Active Patients
|
||||
*
|
||||
* Purpose: Get patients with active status
|
||||
*/
|
||||
export const selectActivePatients = createSelector(
|
||||
[selectPatients],
|
||||
(patients: MedicalCase[]) => patients.filter((patient: MedicalCase) => {
|
||||
// Parse patient details to check status
|
||||
const parseJsonSafely = (jsonString: string | object) => {
|
||||
if (typeof jsonString === 'object') return jsonString;
|
||||
if (typeof jsonString === 'string') {
|
||||
try { return JSON.parse(jsonString); } catch { return {}; }
|
||||
}
|
||||
return {};
|
||||
};
|
||||
const patientDetails = parseJsonSafely(patient.patientdetails);
|
||||
const patientData = patientDetails.patientdetails || patientDetails;
|
||||
return patientData.Status === 'Active';
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Patients by Department
|
||||
*
|
||||
* Purpose: Get patients grouped by department
|
||||
*/
|
||||
export const selectPatientsByDepartment = createSelector(
|
||||
[selectPatients],
|
||||
(patients: MedicalCase[]) => {
|
||||
const grouped: { [key: string]: MedicalCase[] } = {};
|
||||
|
||||
patients.forEach((patient: MedicalCase) => {
|
||||
const dept = patient.type; // Use case type instead of department
|
||||
if (!grouped[dept]) {
|
||||
grouped[dept] = [];
|
||||
}
|
||||
grouped[dept].push(patient);
|
||||
});
|
||||
|
||||
return grouped;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Patient Statistics
|
||||
*
|
||||
* Purpose: Get statistics about patients
|
||||
*/
|
||||
export const selectPatientStats = createSelector(
|
||||
[selectPatients],
|
||||
(patients: MedicalCase[]) => {
|
||||
const total = patients.length;
|
||||
const critical = patients.filter((p: MedicalCase) => p.type === 'Critical').length;
|
||||
const emergency = patients.filter((p: MedicalCase) => p.type === 'Emergency').length;
|
||||
const routine = patients.filter((p: MedicalCase) => p.type === 'Routine').length;
|
||||
|
||||
// Parse patient details for age calculation
|
||||
const parseJsonSafely = (jsonString: string | object) => {
|
||||
if (typeof jsonString === 'object') return jsonString;
|
||||
if (typeof jsonString === 'string') {
|
||||
try { return JSON.parse(jsonString); } catch { return {}; }
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const totalAge = patients.reduce((sum: number, patient: MedicalCase) => {
|
||||
const patientDetails = parseJsonSafely(patient.patientdetails);
|
||||
const patientData = patientDetails.patientdetails || patientDetails;
|
||||
return sum + parseInt(patientData.PatAge || '0');
|
||||
}, 0);
|
||||
const averageAge = total > 0 ? Math.round(totalAge / total) : 0;
|
||||
|
||||
// Case type distribution
|
||||
const caseTypes: { [key: string]: number } = {};
|
||||
patients.forEach((patient: MedicalCase) => {
|
||||
caseTypes[patient.type] = (caseTypes[patient.type] || 0) + 1;
|
||||
});
|
||||
|
||||
return {
|
||||
total,
|
||||
critical,
|
||||
emergency,
|
||||
routine,
|
||||
averageAge,
|
||||
caseTypes,
|
||||
criticalPercentage: total > 0 ? Math.round((critical / total) * 100) : 0,
|
||||
emergencyPercentage: total > 0 ? Math.round((emergency / total) * 100) : 0,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Patient by ID
|
||||
*
|
||||
* Purpose: Get a specific patient by ID
|
||||
*
|
||||
* @param patientId - The ID of the patient to find
|
||||
*/
|
||||
export const selectPatientById = (patientId: string) =>
|
||||
createSelector(
|
||||
[selectPatients],
|
||||
(patients) => patients.find(patient => patient.id === patientId)
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Patients Need Attention
|
||||
*
|
||||
* Purpose: Get patients that need immediate attention
|
||||
*/
|
||||
export const selectPatientsNeedAttention = createSelector(
|
||||
[selectPatients],
|
||||
(patients) => {
|
||||
return patients.filter(patient => {
|
||||
// Critical patients always need attention
|
||||
if (patient.priority === 'CRITICAL') return true;
|
||||
|
||||
// Check vital signs for abnormal values
|
||||
const vitals = patient.vitalSigns;
|
||||
|
||||
// Check blood pressure (hypertensive crisis)
|
||||
if (vitals.bloodPressure.systolic > 180 || vitals.bloodPressure.diastolic > 120) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check heart rate (too high or too low)
|
||||
if (vitals.heartRate.value > 120 || vitals.heartRate.value < 50) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check temperature (fever or hypothermia)
|
||||
if (vitals.temperature.value > 38.5 || vitals.temperature.value < 35) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check oxygen saturation (low)
|
||||
if (vitals.oxygenSaturation.value < 90) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Has Data
|
||||
*
|
||||
* Purpose: Check if we have patient data
|
||||
*/
|
||||
export const selectHasPatientData = createSelector(
|
||||
[selectPatients],
|
||||
(patients) => patients.length > 0
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Is Empty State
|
||||
*
|
||||
* Purpose: Check if we should show empty state
|
||||
*/
|
||||
export const selectIsEmptyState = createSelector(
|
||||
[selectPatients, selectPatientsLoading, selectFilteredPatients],
|
||||
(patients, isLoading, filteredPatients) =>
|
||||
!isLoading && patients.length > 0 && filteredPatients.length === 0
|
||||
);
|
||||
|
||||
/*
|
||||
* End of File: patientCareSelectors.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
@ -6,7 +6,8 @@
|
||||
*/
|
||||
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { Patient, PatientCareState } from '../../../shared/types';
|
||||
import { MedicalCase, PatientCareState } from '../../../shared/types';
|
||||
import { patientAPI } from '../services/patientAPI';
|
||||
|
||||
// ============================================================================
|
||||
// ASYNC THUNKS
|
||||
@ -21,92 +22,92 @@ import { Patient, PatientCareState } from '../../../shared/types';
|
||||
*/
|
||||
export const fetchPatients = createAsyncThunk(
|
||||
'patientCare/fetchPatients',
|
||||
async (_, { rejectWithValue }) => {
|
||||
async (token: string, { rejectWithValue }) => {
|
||||
try {
|
||||
// TODO: Replace with actual API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
// Mock patients data
|
||||
const mockPatients: Patient[] = [
|
||||
{
|
||||
id: '1',
|
||||
mrn: 'MRN001',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
dateOfBirth: new Date('1985-03-15'),
|
||||
gender: 'MALE',
|
||||
age: 38,
|
||||
bedNumber: 'A1',
|
||||
roomNumber: '101',
|
||||
admissionDate: new Date('2024-01-15'),
|
||||
status: 'ACTIVE',
|
||||
priority: 'CRITICAL',
|
||||
department: 'Emergency',
|
||||
attendingPhysician: 'Dr. Smith',
|
||||
allergies: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Penicillin',
|
||||
severity: 'SEVERE',
|
||||
reaction: 'Anaphylaxis',
|
||||
},
|
||||
],
|
||||
medications: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Morphine',
|
||||
dosage: '2mg',
|
||||
frequency: 'Every 4 hours',
|
||||
route: 'IV',
|
||||
startDate: new Date(),
|
||||
status: 'ACTIVE',
|
||||
prescribedBy: 'Dr. Smith',
|
||||
},
|
||||
],
|
||||
vitalSigns: {
|
||||
bloodPressure: { systolic: 140, diastolic: 90, timestamp: new Date() },
|
||||
heartRate: { value: 95, timestamp: new Date() },
|
||||
temperature: { value: 37.2, timestamp: new Date() },
|
||||
respiratoryRate: { value: 18, timestamp: new Date() },
|
||||
oxygenSaturation: { value: 98, timestamp: new Date() },
|
||||
// Make actual API call to fetch medical cases
|
||||
const response :any = await patientAPI.getPatients(token);
|
||||
console.log('patients response',response)
|
||||
if (response.ok && response.data&&response.data.success) {
|
||||
// Add random case types to each patient record
|
||||
const caseTypes: Array<'Critical' | 'Emergency' | 'Routine'> = ['Critical', 'Emergency', 'Routine'];
|
||||
|
||||
const patientsWithTypes = response.data.data.map((patient: any) => ({
|
||||
...patient,
|
||||
type: caseTypes[Math.floor(Math.random() * caseTypes.length)]
|
||||
}));
|
||||
|
||||
console.log('patients with random types', patientsWithTypes);
|
||||
return patientsWithTypes as MedicalCase[];
|
||||
} else {
|
||||
// Fallback to mock data for development
|
||||
const mockPatients: MedicalCase[] = [
|
||||
{
|
||||
id: 1,
|
||||
patientdetails: JSON.stringify({
|
||||
patientdetails: {
|
||||
Date: '2024-01-15',
|
||||
Name: 'John Doe',
|
||||
PatID: 'MRN001',
|
||||
PatAge: '38',
|
||||
PatSex: 'M',
|
||||
Status: 'Active',
|
||||
InstName: 'City General Hospital',
|
||||
Modality: 'CT',
|
||||
ReportStatus: 'Pending'
|
||||
}
|
||||
}),
|
||||
series: JSON.stringify([
|
||||
{
|
||||
Path: ['/dicom/series1'],
|
||||
SerDes: 'Chest CT',
|
||||
ViePos: 'Supine',
|
||||
pngpath: '/images/ct_chest_1.png',
|
||||
SeriesNum: '1',
|
||||
ImgTotalinSeries: '50'
|
||||
}
|
||||
]),
|
||||
created_at: '2024-01-15T10:30:00Z',
|
||||
updated_at: '2024-01-15T11:45:00Z',
|
||||
series_id: 'series_001',
|
||||
type: 'Critical'
|
||||
},
|
||||
medicalHistory: [],
|
||||
currentDiagnosis: 'Chest pain, rule out MI',
|
||||
lastUpdated: new Date(),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
mrn: 'MRN002',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
dateOfBirth: new Date('1990-07-22'),
|
||||
gender: 'FEMALE',
|
||||
age: 33,
|
||||
bedNumber: 'B2',
|
||||
roomNumber: '102',
|
||||
admissionDate: new Date('2024-01-15'),
|
||||
status: 'ACTIVE',
|
||||
priority: 'HIGH',
|
||||
department: 'Trauma',
|
||||
attendingPhysician: 'Dr. Johnson',
|
||||
allergies: [],
|
||||
medications: [],
|
||||
vitalSigns: {
|
||||
bloodPressure: { systolic: 120, diastolic: 80, timestamp: new Date() },
|
||||
heartRate: { value: 88, timestamp: new Date() },
|
||||
temperature: { value: 36.8, timestamp: new Date() },
|
||||
respiratoryRate: { value: 16, timestamp: new Date() },
|
||||
oxygenSaturation: { value: 99, timestamp: new Date() },
|
||||
{
|
||||
id: 2,
|
||||
patientdetails: JSON.stringify({
|
||||
patientdetails: {
|
||||
Date: '2024-01-15',
|
||||
Name: 'Jane Smith',
|
||||
PatID: 'MRN002',
|
||||
PatAge: '33',
|
||||
PatSex: 'F',
|
||||
Status: 'Active',
|
||||
InstName: 'Memorial Medical Center',
|
||||
Modality: 'MR',
|
||||
ReportStatus: 'Completed'
|
||||
}
|
||||
}),
|
||||
series: JSON.stringify([
|
||||
{
|
||||
Path: ['/dicom/series2'],
|
||||
SerDes: 'Brain MRI',
|
||||
ViePos: 'Supine',
|
||||
pngpath: '/images/mri_brain_1.png',
|
||||
SeriesNum: '2',
|
||||
ImgTotalinSeries: '120'
|
||||
}
|
||||
]),
|
||||
created_at: '2024-01-15T09:15:00Z',
|
||||
updated_at: '2024-01-15T10:30:00Z',
|
||||
series_id: 'series_002',
|
||||
type: 'Routine'
|
||||
},
|
||||
medicalHistory: [],
|
||||
currentDiagnosis: 'Multiple trauma from MVA',
|
||||
lastUpdated: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
return mockPatients;
|
||||
} catch (error) {
|
||||
return rejectWithValue('Failed to fetch patients.');
|
||||
];
|
||||
|
||||
return mockPatients;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Fetch patients error:', error);
|
||||
return rejectWithValue(error.message || 'Failed to fetch patients.');
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -124,54 +125,38 @@ export const fetchPatientDetails = createAsyncThunk(
|
||||
async (patientId: string, { rejectWithValue }) => {
|
||||
try {
|
||||
// TODO: Replace with actual API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
// Mock patient details (same as above but with more detailed info)
|
||||
const mockPatient: Patient = {
|
||||
id: patientId,
|
||||
mrn: `MRN${patientId.padStart(3, '0')}`,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
dateOfBirth: new Date('1985-03-15'),
|
||||
gender: 'MALE',
|
||||
age: 38,
|
||||
bedNumber: 'A1',
|
||||
roomNumber: '101',
|
||||
admissionDate: new Date('2024-01-15'),
|
||||
status: 'ACTIVE',
|
||||
priority: 'CRITICAL',
|
||||
department: 'Emergency',
|
||||
attendingPhysician: 'Dr. Smith',
|
||||
allergies: [
|
||||
// Mock patient details for specific patient
|
||||
const mockPatient: MedicalCase = {
|
||||
id: parseInt(patientId),
|
||||
patientdetails: JSON.stringify({
|
||||
patientdetails: {
|
||||
Date: '2024-01-15',
|
||||
Name: 'John Doe',
|
||||
PatID: `MRN${patientId.padStart(3, '0')}`,
|
||||
PatAge: '38',
|
||||
PatSex: 'M',
|
||||
Status: 'Active',
|
||||
InstName: 'City General Hospital',
|
||||
Modality: 'CT',
|
||||
ReportStatus: 'Pending'
|
||||
}
|
||||
}),
|
||||
series: JSON.stringify([
|
||||
{
|
||||
id: '1',
|
||||
name: 'Penicillin',
|
||||
severity: 'SEVERE',
|
||||
reaction: 'Anaphylaxis',
|
||||
},
|
||||
],
|
||||
medications: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Morphine',
|
||||
dosage: '2mg',
|
||||
frequency: 'Every 4 hours',
|
||||
route: 'IV',
|
||||
startDate: new Date(),
|
||||
status: 'ACTIVE',
|
||||
prescribedBy: 'Dr. Smith',
|
||||
},
|
||||
],
|
||||
vitalSigns: {
|
||||
bloodPressure: { systolic: 140, diastolic: 90, timestamp: new Date() },
|
||||
heartRate: { value: 95, timestamp: new Date() },
|
||||
temperature: { value: 37.2, timestamp: new Date() },
|
||||
respiratoryRate: { value: 18, timestamp: new Date() },
|
||||
oxygenSaturation: { value: 98, timestamp: new Date() },
|
||||
},
|
||||
medicalHistory: [],
|
||||
currentDiagnosis: 'Chest pain, rule out MI',
|
||||
lastUpdated: new Date(),
|
||||
Path: [`/dicom/series${patientId}`],
|
||||
SerDes: 'Chest CT',
|
||||
ViePos: 'Supine',
|
||||
pngpath: `/images/ct_chest_${patientId}.png`,
|
||||
SeriesNum: patientId,
|
||||
ImgTotalinSeries: '50'
|
||||
}
|
||||
]),
|
||||
created_at: '2024-01-15T10:30:00Z',
|
||||
updated_at: '2024-01-15T11:45:00Z',
|
||||
series_id: `series_${patientId.padStart(3, '0')}`,
|
||||
type: 'Critical'
|
||||
};
|
||||
|
||||
return mockPatient;
|
||||
@ -191,10 +176,10 @@ export const fetchPatientDetails = createAsyncThunk(
|
||||
*/
|
||||
export const updatePatient = createAsyncThunk(
|
||||
'patientCare/updatePatient',
|
||||
async (patientData: Partial<Patient> & { id: string }, { rejectWithValue }) => {
|
||||
async (patientData: Partial<MedicalCase> & { id: number }, { rejectWithValue }) => {
|
||||
try {
|
||||
// TODO: Replace with actual API call
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
return patientData;
|
||||
} catch (error) {
|
||||
return rejectWithValue('Failed to update patient.');
|
||||
@ -234,7 +219,7 @@ const initialState: PatientCareState = {
|
||||
// Search and filtering
|
||||
searchQuery: '',
|
||||
selectedFilter: 'all',
|
||||
sortBy: 'priority',
|
||||
sortBy: 'date',
|
||||
sortOrder: 'desc',
|
||||
|
||||
// Pagination
|
||||
@ -292,7 +277,7 @@ const patientCareSlice = createSlice({
|
||||
*
|
||||
* Purpose: Set patient filter
|
||||
*/
|
||||
setFilter: (state, action: PayloadAction<'all' | 'active' | 'discharged' | 'critical'>) => {
|
||||
setFilter: (state, action: PayloadAction<'all' | 'Critical' | 'Routine' | 'Emergency'>) => {
|
||||
state.selectedFilter = action.payload;
|
||||
state.currentPage = 1; // Reset to first page when filtering
|
||||
},
|
||||
@ -302,7 +287,7 @@ const patientCareSlice = createSlice({
|
||||
*
|
||||
* Purpose: Set patient sort options
|
||||
*/
|
||||
setSort: (state, action: PayloadAction<{ by: string; order: 'asc' | 'desc' }>) => {
|
||||
setSort: (state, action: PayloadAction<{ by: 'date' | 'name' | 'age'; order: 'asc' | 'desc' }>) => {
|
||||
state.sortBy = action.payload.by;
|
||||
state.sortOrder = action.payload.order;
|
||||
},
|
||||
@ -331,7 +316,7 @@ const patientCareSlice = createSlice({
|
||||
*
|
||||
* Purpose: Set the currently selected patient
|
||||
*/
|
||||
setCurrentPatient: (state, action: PayloadAction<Patient | null>) => {
|
||||
setCurrentPatient: (state, action: PayloadAction<MedicalCase | null>) => {
|
||||
state.currentPatient = action.payload;
|
||||
},
|
||||
|
||||
@ -340,7 +325,7 @@ const patientCareSlice = createSlice({
|
||||
*
|
||||
* Purpose: Update a patient in the patients list
|
||||
*/
|
||||
updatePatientInList: (state, action: PayloadAction<Patient>) => {
|
||||
updatePatientInList: (state, action: PayloadAction<MedicalCase>) => {
|
||||
const index = state.patients.findIndex(patient => patient.id === action.payload.id);
|
||||
if (index !== -1) {
|
||||
state.patients[index] = action.payload;
|
||||
@ -357,7 +342,7 @@ const patientCareSlice = createSlice({
|
||||
*
|
||||
* Purpose: Add a new patient to the list
|
||||
*/
|
||||
addPatient: (state, action: PayloadAction<Patient>) => {
|
||||
addPatient: (state, action: PayloadAction<MedicalCase>) => {
|
||||
state.patients.unshift(action.payload);
|
||||
state.totalItems += 1;
|
||||
},
|
||||
@ -367,7 +352,7 @@ const patientCareSlice = createSlice({
|
||||
*
|
||||
* Purpose: Remove a patient from the list
|
||||
*/
|
||||
removePatient: (state, action: PayloadAction<string>) => {
|
||||
removePatient: (state, action: PayloadAction<number>) => {
|
||||
const index = state.patients.findIndex(patient => patient.id === action.payload);
|
||||
if (index !== -1) {
|
||||
state.patients.splice(index, 1);
|
||||
|
||||
621
app/modules/PatientCare/screens/PatientsScreen.tsx
Normal file
621
app/modules/PatientCare/screens/PatientsScreen.tsx
Normal file
@ -0,0 +1,621 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
// Get screen dimensions
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
interface PatientsScreenProps {
|
||||
navigation: any;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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: patient.id,
|
||||
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
|
||||
navigation.navigate('Notifications');
|
||||
}}
|
||||
>
|
||||
<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.
|
||||
*/
|
||||
14
app/modules/PatientCare/screens/index.ts
Normal file
14
app/modules/PatientCare/screens/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* File: index.ts
|
||||
* Description: Barrel export for PatientCare screens
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
export { default as PatientsScreen } from './PatientsScreen';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
14
app/modules/PatientCare/services/index.ts
Normal file
14
app/modules/PatientCare/services/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* File: index.ts
|
||||
* Description: Barrel export for Dashboard services
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
export * from './patientAPI';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
168
app/modules/PatientCare/services/patientAPI.ts
Normal file
168
app/modules/PatientCare/services/patientAPI.ts
Normal file
@ -0,0 +1,168 @@
|
||||
/*
|
||||
* File: patientAPI.ts
|
||||
* Description: API service for patient care operations using apisauce
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import { create } from 'apisauce';
|
||||
import { API_CONFIG, buildHeaders } from '../../../shared/utils';
|
||||
|
||||
const api = create({
|
||||
baseURL: API_CONFIG.BASE_URL
|
||||
});
|
||||
|
||||
/**
|
||||
* Patient API Service
|
||||
*
|
||||
* Purpose: Handle all patient-related API operations
|
||||
*
|
||||
* Features:
|
||||
* - Get patient list with filtering
|
||||
* - Get individual patient details
|
||||
* - Update patient information
|
||||
* - Get patient vital signs
|
||||
* - Get patient medical history
|
||||
*/
|
||||
export const patientAPI = {
|
||||
/**
|
||||
* Get Patients
|
||||
*
|
||||
* Purpose: Fetch list of medical cases from server
|
||||
*
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with medical cases data
|
||||
*/
|
||||
getPatients: (token: string) => {
|
||||
return api.get('/api/dicom/medpacks-sync/get-synced-medpacks-data', {}, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Patient Details
|
||||
*
|
||||
* Purpose: Fetch detailed information for a specific patient
|
||||
*
|
||||
* @param patientId - Patient ID
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with patient details
|
||||
*/
|
||||
getPatientDetails: (patientId: string, token: string) => {
|
||||
return api.get(`/api/patients/${patientId}`, {}, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Update Patient
|
||||
*
|
||||
* Purpose: Update patient information
|
||||
*
|
||||
* @param patientId - Patient ID
|
||||
* @param patientData - Updated patient data
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with updated patient data
|
||||
*/
|
||||
updatePatient: (patientId: string, patientData: any, token: string) => {
|
||||
return api.put(`/api/patients/${patientId}`, patientData, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Patient Vital Signs
|
||||
*
|
||||
* Purpose: Fetch latest vital signs for a patient
|
||||
*
|
||||
* @param patientId - Patient ID
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with vital signs data
|
||||
*/
|
||||
getPatientVitals: (patientId: string, token: string) => {
|
||||
return api.get(`/api/patients/${patientId}/vitals`, {}, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Update Patient Vital Signs
|
||||
*
|
||||
* Purpose: Add new vital signs reading for a patient
|
||||
*
|
||||
* @param patientId - Patient ID
|
||||
* @param vitalSigns - Vital signs data
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with updated vital signs
|
||||
*/
|
||||
updatePatientVitals: (patientId: string, vitalSigns: any, token: string) => {
|
||||
return api.post(`/api/patients/${patientId}/vitals`, vitalSigns, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Patient Medical History
|
||||
*
|
||||
* Purpose: Fetch medical history for a patient
|
||||
*
|
||||
* @param patientId - Patient ID
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with medical history data
|
||||
*/
|
||||
getPatientHistory: (patientId: string, token: string) => {
|
||||
return api.get(`/api/patients/${patientId}/history`, {}, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Patient Medications
|
||||
*
|
||||
* Purpose: Fetch current medications for a patient
|
||||
*
|
||||
* @param patientId - Patient ID
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with medications data
|
||||
*/
|
||||
getPatientMedications: (patientId: string, token: string) => {
|
||||
return api.get(`/api/patients/${patientId}/medications`, {}, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Add Patient Medication
|
||||
*
|
||||
* Purpose: Add new medication for a patient
|
||||
*
|
||||
* @param patientId - Patient ID
|
||||
* @param medication - Medication data
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with updated medications
|
||||
*/
|
||||
addPatientMedication: (patientId: string, medication: any, token: string) => {
|
||||
return api.post(`/api/patients/${patientId}/medications`, medication, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Patient Allergies
|
||||
*
|
||||
* Purpose: Fetch allergies for a patient
|
||||
*
|
||||
* @param patientId - Patient ID
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with allergies data
|
||||
*/
|
||||
getPatientAllergies: (patientId: string, token: string) => {
|
||||
return api.get(`/api/patients/${patientId}/allergies`, {}, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Search Patients
|
||||
*
|
||||
* Purpose: Search patients by various criteria
|
||||
*
|
||||
* @param query - Search query
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with search results
|
||||
*/
|
||||
searchPatients: (query: string, token: string) => {
|
||||
return api.get('/api/patients/search', { q: query }, buildHeaders({ token }));
|
||||
},
|
||||
};
|
||||
|
||||
// Legacy export for backward compatibility
|
||||
export const caseAPI = patientAPI;
|
||||
|
||||
/*
|
||||
* End of File: patientAPI.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
@ -13,6 +13,7 @@ import { SettingsStackNavigator } from '../modules/Settings/navigation';
|
||||
import { MainTabParamList } from './navigationTypes';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import { ComingSoonScreen } from '../shared/components';
|
||||
import { PatientsScreen } from '../modules/PatientCare';
|
||||
|
||||
// Create the bottom tab navigator
|
||||
const Tab = createBottomTabNavigator<MainTabParamList>();
|
||||
@ -83,7 +84,7 @@ export const MainTabNavigator: React.FC = () => {
|
||||
{/* Patients Tab - Patient list and management */}
|
||||
<Tab.Screen
|
||||
name="Patients"
|
||||
component={ComingSoonScreen} // TODO: Replace with actual PatientsScreen
|
||||
component={PatientsScreen} // TODO: Replace with actual PatientsScreen
|
||||
options={{
|
||||
title: 'Patient List',
|
||||
tabBarLabel: 'Patients',
|
||||
|
||||
@ -5,29 +5,43 @@
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
export interface Patient {
|
||||
id: string;
|
||||
mrn: string; // Medical Record Number
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
dateOfBirth: Date;
|
||||
gender: 'MALE' | 'FEMALE' | 'OTHER';
|
||||
age: number;
|
||||
bedNumber: string;
|
||||
roomNumber: string;
|
||||
admissionDate: Date;
|
||||
status: PatientStatus;
|
||||
priority: AlertPriority;
|
||||
department: string;
|
||||
attendingPhysician: string;
|
||||
allergies: Allergy[];
|
||||
medications: Medication[];
|
||||
vitalSigns: VitalSigns;
|
||||
medicalHistory: MedicalHistory[];
|
||||
currentDiagnosis: string;
|
||||
lastUpdated: Date;
|
||||
// DICOM Patient Details Interface
|
||||
export interface PatientDetails {
|
||||
Date: string;
|
||||
Name: string;
|
||||
PatID: string;
|
||||
PatAge: string;
|
||||
PatSex: string;
|
||||
Status: string;
|
||||
InstName: string;
|
||||
Modality: 'DX' | 'CT' | 'MR';
|
||||
ReportStatus: string | null;
|
||||
}
|
||||
|
||||
// DICOM Series Interface
|
||||
export interface Series {
|
||||
Path: string[];
|
||||
SerDes: string;
|
||||
ViePos: string | null;
|
||||
pngpath: string;
|
||||
SeriesNum: string;
|
||||
ImgTotalinSeries: string;
|
||||
}
|
||||
|
||||
// Medical Case Interface (Main Patient Record)
|
||||
export interface MedicalCase {
|
||||
id: number;
|
||||
patientdetails: PatientDetails | string; // Can be object or JSON string
|
||||
series: Series[] | string; // Can be array or JSON string
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
series_id: string | null;
|
||||
type: 'Critical' | 'Routine' | 'Emergency';
|
||||
}
|
||||
|
||||
// Legacy Patient interface for backward compatibility
|
||||
export interface Patient extends MedicalCase {}
|
||||
|
||||
export type PatientStatus =
|
||||
| 'ACTIVE'
|
||||
| 'PENDING'
|
||||
@ -111,6 +125,35 @@ export interface ScanResult {
|
||||
priority: AlertPriority;
|
||||
}
|
||||
|
||||
export interface PatientCareState {
|
||||
// Patients data (Medical Cases)
|
||||
patients: MedicalCase[];
|
||||
currentPatient: MedicalCase | null;
|
||||
|
||||
// Loading states
|
||||
isLoading: boolean;
|
||||
isRefreshing: boolean;
|
||||
isLoadingPatientDetails: boolean;
|
||||
|
||||
// Error handling
|
||||
error: string | null;
|
||||
|
||||
// Search and filtering
|
||||
searchQuery: string;
|
||||
selectedFilter: 'all' | 'Critical' | 'Routine' | 'Emergency';
|
||||
sortBy: 'date' | 'name' | 'age';
|
||||
sortOrder: 'asc' | 'desc';
|
||||
|
||||
// Pagination
|
||||
currentPage: number;
|
||||
itemsPerPage: number;
|
||||
totalItems: number;
|
||||
|
||||
// Cache
|
||||
lastUpdated: Date | null;
|
||||
cacheExpiry: Date | null;
|
||||
}
|
||||
|
||||
/*
|
||||
* End of File: patient.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
|
||||
Loading…
Reference in New Issue
Block a user