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 { 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
|
// ASYNC THUNKS
|
||||||
@ -21,92 +22,92 @@ import { Patient, PatientCareState } from '../../../shared/types';
|
|||||||
*/
|
*/
|
||||||
export const fetchPatients = createAsyncThunk(
|
export const fetchPatients = createAsyncThunk(
|
||||||
'patientCare/fetchPatients',
|
'patientCare/fetchPatients',
|
||||||
async (_, { rejectWithValue }) => {
|
async (token: string, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
// TODO: Replace with actual API call
|
// Make actual API call to fetch medical cases
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
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'];
|
||||||
|
|
||||||
// Mock patients data
|
const patientsWithTypes = response.data.data.map((patient: any) => ({
|
||||||
const mockPatients: Patient[] = [
|
...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',
|
id: 1,
|
||||||
mrn: 'MRN001',
|
patientdetails: JSON.stringify({
|
||||||
firstName: 'John',
|
patientdetails: {
|
||||||
lastName: 'Doe',
|
Date: '2024-01-15',
|
||||||
dateOfBirth: new Date('1985-03-15'),
|
Name: 'John Doe',
|
||||||
gender: 'MALE',
|
PatID: 'MRN001',
|
||||||
age: 38,
|
PatAge: '38',
|
||||||
bedNumber: 'A1',
|
PatSex: 'M',
|
||||||
roomNumber: '101',
|
Status: 'Active',
|
||||||
admissionDate: new Date('2024-01-15'),
|
InstName: 'City General Hospital',
|
||||||
status: 'ACTIVE',
|
Modality: 'CT',
|
||||||
priority: 'CRITICAL',
|
ReportStatus: 'Pending'
|
||||||
department: 'Emergency',
|
}
|
||||||
attendingPhysician: 'Dr. Smith',
|
}),
|
||||||
allergies: [
|
series: JSON.stringify([
|
||||||
{
|
{
|
||||||
id: '1',
|
Path: ['/dicom/series1'],
|
||||||
name: 'Penicillin',
|
SerDes: 'Chest CT',
|
||||||
severity: 'SEVERE',
|
ViePos: 'Supine',
|
||||||
reaction: 'Anaphylaxis',
|
pngpath: '/images/ct_chest_1.png',
|
||||||
},
|
SeriesNum: '1',
|
||||||
],
|
ImgTotalinSeries: '50'
|
||||||
medications: [
|
}
|
||||||
{
|
]),
|
||||||
id: '1',
|
created_at: '2024-01-15T10:30:00Z',
|
||||||
name: 'Morphine',
|
updated_at: '2024-01-15T11:45:00Z',
|
||||||
dosage: '2mg',
|
series_id: 'series_001',
|
||||||
frequency: 'Every 4 hours',
|
type: 'Critical'
|
||||||
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(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: 2,
|
||||||
mrn: 'MRN002',
|
patientdetails: JSON.stringify({
|
||||||
firstName: 'Jane',
|
patientdetails: {
|
||||||
lastName: 'Smith',
|
Date: '2024-01-15',
|
||||||
dateOfBirth: new Date('1990-07-22'),
|
Name: 'Jane Smith',
|
||||||
gender: 'FEMALE',
|
PatID: 'MRN002',
|
||||||
age: 33,
|
PatAge: '33',
|
||||||
bedNumber: 'B2',
|
PatSex: 'F',
|
||||||
roomNumber: '102',
|
Status: 'Active',
|
||||||
admissionDate: new Date('2024-01-15'),
|
InstName: 'Memorial Medical Center',
|
||||||
status: 'ACTIVE',
|
Modality: 'MR',
|
||||||
priority: 'HIGH',
|
ReportStatus: 'Completed'
|
||||||
department: 'Trauma',
|
}
|
||||||
attendingPhysician: 'Dr. Johnson',
|
}),
|
||||||
allergies: [],
|
series: JSON.stringify([
|
||||||
medications: [],
|
{
|
||||||
vitalSigns: {
|
Path: ['/dicom/series2'],
|
||||||
bloodPressure: { systolic: 120, diastolic: 80, timestamp: new Date() },
|
SerDes: 'Brain MRI',
|
||||||
heartRate: { value: 88, timestamp: new Date() },
|
ViePos: 'Supine',
|
||||||
temperature: { value: 36.8, timestamp: new Date() },
|
pngpath: '/images/mri_brain_1.png',
|
||||||
respiratoryRate: { value: 16, timestamp: new Date() },
|
SeriesNum: '2',
|
||||||
oxygenSaturation: { value: 99, timestamp: new Date() },
|
ImgTotalinSeries: '120'
|
||||||
},
|
}
|
||||||
medicalHistory: [],
|
]),
|
||||||
currentDiagnosis: 'Multiple trauma from MVA',
|
created_at: '2024-01-15T09:15:00Z',
|
||||||
lastUpdated: new Date(),
|
updated_at: '2024-01-15T10:30:00Z',
|
||||||
|
series_id: 'series_002',
|
||||||
|
type: 'Routine'
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return mockPatients;
|
return mockPatients;
|
||||||
} catch (error) {
|
}
|
||||||
return rejectWithValue('Failed to fetch patients.');
|
} 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 }) => {
|
async (patientId: string, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
// TODO: Replace with actual API call
|
// 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)
|
// Mock patient details for specific patient
|
||||||
const mockPatient: Patient = {
|
const mockPatient: MedicalCase = {
|
||||||
id: patientId,
|
id: parseInt(patientId),
|
||||||
mrn: `MRN${patientId.padStart(3, '0')}`,
|
patientdetails: JSON.stringify({
|
||||||
firstName: 'John',
|
patientdetails: {
|
||||||
lastName: 'Doe',
|
Date: '2024-01-15',
|
||||||
dateOfBirth: new Date('1985-03-15'),
|
Name: 'John Doe',
|
||||||
gender: 'MALE',
|
PatID: `MRN${patientId.padStart(3, '0')}`,
|
||||||
age: 38,
|
PatAge: '38',
|
||||||
bedNumber: 'A1',
|
PatSex: 'M',
|
||||||
roomNumber: '101',
|
Status: 'Active',
|
||||||
admissionDate: new Date('2024-01-15'),
|
InstName: 'City General Hospital',
|
||||||
status: 'ACTIVE',
|
Modality: 'CT',
|
||||||
priority: 'CRITICAL',
|
ReportStatus: 'Pending'
|
||||||
department: 'Emergency',
|
}
|
||||||
attendingPhysician: 'Dr. Smith',
|
}),
|
||||||
allergies: [
|
series: JSON.stringify([
|
||||||
{
|
{
|
||||||
id: '1',
|
Path: [`/dicom/series${patientId}`],
|
||||||
name: 'Penicillin',
|
SerDes: 'Chest CT',
|
||||||
severity: 'SEVERE',
|
ViePos: 'Supine',
|
||||||
reaction: 'Anaphylaxis',
|
pngpath: `/images/ct_chest_${patientId}.png`,
|
||||||
},
|
SeriesNum: patientId,
|
||||||
],
|
ImgTotalinSeries: '50'
|
||||||
medications: [
|
}
|
||||||
{
|
]),
|
||||||
id: '1',
|
created_at: '2024-01-15T10:30:00Z',
|
||||||
name: 'Morphine',
|
updated_at: '2024-01-15T11:45:00Z',
|
||||||
dosage: '2mg',
|
series_id: `series_${patientId.padStart(3, '0')}`,
|
||||||
frequency: 'Every 4 hours',
|
type: 'Critical'
|
||||||
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(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return mockPatient;
|
return mockPatient;
|
||||||
@ -191,10 +176,10 @@ export const fetchPatientDetails = createAsyncThunk(
|
|||||||
*/
|
*/
|
||||||
export const updatePatient = createAsyncThunk(
|
export const updatePatient = createAsyncThunk(
|
||||||
'patientCare/updatePatient',
|
'patientCare/updatePatient',
|
||||||
async (patientData: Partial<Patient> & { id: string }, { rejectWithValue }) => {
|
async (patientData: Partial<MedicalCase> & { id: number }, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
// TODO: Replace with actual API call
|
// TODO: Replace with actual API call
|
||||||
await new Promise(resolve => setTimeout(resolve, 800));
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
return patientData;
|
return patientData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return rejectWithValue('Failed to update patient.');
|
return rejectWithValue('Failed to update patient.');
|
||||||
@ -234,7 +219,7 @@ const initialState: PatientCareState = {
|
|||||||
// Search and filtering
|
// Search and filtering
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
selectedFilter: 'all',
|
selectedFilter: 'all',
|
||||||
sortBy: 'priority',
|
sortBy: 'date',
|
||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
@ -292,7 +277,7 @@ const patientCareSlice = createSlice({
|
|||||||
*
|
*
|
||||||
* Purpose: Set patient filter
|
* 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.selectedFilter = action.payload;
|
||||||
state.currentPage = 1; // Reset to first page when filtering
|
state.currentPage = 1; // Reset to first page when filtering
|
||||||
},
|
},
|
||||||
@ -302,7 +287,7 @@ const patientCareSlice = createSlice({
|
|||||||
*
|
*
|
||||||
* Purpose: Set patient sort options
|
* 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.sortBy = action.payload.by;
|
||||||
state.sortOrder = action.payload.order;
|
state.sortOrder = action.payload.order;
|
||||||
},
|
},
|
||||||
@ -331,7 +316,7 @@ const patientCareSlice = createSlice({
|
|||||||
*
|
*
|
||||||
* Purpose: Set the currently selected patient
|
* Purpose: Set the currently selected patient
|
||||||
*/
|
*/
|
||||||
setCurrentPatient: (state, action: PayloadAction<Patient | null>) => {
|
setCurrentPatient: (state, action: PayloadAction<MedicalCase | null>) => {
|
||||||
state.currentPatient = action.payload;
|
state.currentPatient = action.payload;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -340,7 +325,7 @@ const patientCareSlice = createSlice({
|
|||||||
*
|
*
|
||||||
* Purpose: Update a patient in the patients list
|
* 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);
|
const index = state.patients.findIndex(patient => patient.id === action.payload.id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
state.patients[index] = action.payload;
|
state.patients[index] = action.payload;
|
||||||
@ -357,7 +342,7 @@ const patientCareSlice = createSlice({
|
|||||||
*
|
*
|
||||||
* Purpose: Add a new patient to the list
|
* Purpose: Add a new patient to the list
|
||||||
*/
|
*/
|
||||||
addPatient: (state, action: PayloadAction<Patient>) => {
|
addPatient: (state, action: PayloadAction<MedicalCase>) => {
|
||||||
state.patients.unshift(action.payload);
|
state.patients.unshift(action.payload);
|
||||||
state.totalItems += 1;
|
state.totalItems += 1;
|
||||||
},
|
},
|
||||||
@ -367,7 +352,7 @@ const patientCareSlice = createSlice({
|
|||||||
*
|
*
|
||||||
* Purpose: Remove a patient from the list
|
* 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);
|
const index = state.patients.findIndex(patient => patient.id === action.payload);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
state.patients.splice(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 { MainTabParamList } from './navigationTypes';
|
||||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||||
import { ComingSoonScreen } from '../shared/components';
|
import { ComingSoonScreen } from '../shared/components';
|
||||||
|
import { PatientsScreen } from '../modules/PatientCare';
|
||||||
|
|
||||||
// Create the bottom tab navigator
|
// Create the bottom tab navigator
|
||||||
const Tab = createBottomTabNavigator<MainTabParamList>();
|
const Tab = createBottomTabNavigator<MainTabParamList>();
|
||||||
@ -83,7 +84,7 @@ export const MainTabNavigator: React.FC = () => {
|
|||||||
{/* Patients Tab - Patient list and management */}
|
{/* Patients Tab - Patient list and management */}
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="Patients"
|
name="Patients"
|
||||||
component={ComingSoonScreen} // TODO: Replace with actual PatientsScreen
|
component={PatientsScreen} // TODO: Replace with actual PatientsScreen
|
||||||
options={{
|
options={{
|
||||||
title: 'Patient List',
|
title: 'Patient List',
|
||||||
tabBarLabel: 'Patients',
|
tabBarLabel: 'Patients',
|
||||||
|
|||||||
@ -5,29 +5,43 @@
|
|||||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface Patient {
|
// DICOM Patient Details Interface
|
||||||
id: string;
|
export interface PatientDetails {
|
||||||
mrn: string; // Medical Record Number
|
Date: string;
|
||||||
firstName: string;
|
Name: string;
|
||||||
lastName: string;
|
PatID: string;
|
||||||
dateOfBirth: Date;
|
PatAge: string;
|
||||||
gender: 'MALE' | 'FEMALE' | 'OTHER';
|
PatSex: string;
|
||||||
age: number;
|
Status: string;
|
||||||
bedNumber: string;
|
InstName: string;
|
||||||
roomNumber: string;
|
Modality: 'DX' | 'CT' | 'MR';
|
||||||
admissionDate: Date;
|
ReportStatus: string | null;
|
||||||
status: PatientStatus;
|
|
||||||
priority: AlertPriority;
|
|
||||||
department: string;
|
|
||||||
attendingPhysician: string;
|
|
||||||
allergies: Allergy[];
|
|
||||||
medications: Medication[];
|
|
||||||
vitalSigns: VitalSigns;
|
|
||||||
medicalHistory: MedicalHistory[];
|
|
||||||
currentDiagnosis: string;
|
|
||||||
lastUpdated: Date;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 =
|
export type PatientStatus =
|
||||||
| 'ACTIVE'
|
| 'ACTIVE'
|
||||||
| 'PENDING'
|
| 'PENDING'
|
||||||
@ -111,6 +125,35 @@ export interface ScanResult {
|
|||||||
priority: AlertPriority;
|
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
|
* End of File: patient.ts
|
||||||
* Design & Developed by Tech4Biz Solutions
|
* Design & Developed by Tech4Biz Solutions
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user