patient data fetched to patient tab

This commit is contained in:
yashwin-foxy 2025-08-07 19:42:41 +05:30
parent 9eb1416866
commit 84b63e401f
15 changed files with 2779 additions and 161 deletions

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View File

@ -6,7 +6,8 @@
*/
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { Patient, PatientCareState } from '../../../shared/types';
import { MedicalCase, PatientCareState } from '../../../shared/types';
import { patientAPI } from '../services/patientAPI';
// ============================================================================
// ASYNC THUNKS
@ -21,92 +22,92 @@ import { Patient, PatientCareState } from '../../../shared/types';
*/
export const fetchPatients = createAsyncThunk(
'patientCare/fetchPatients',
async (_, { rejectWithValue }) => {
async (token: string, { rejectWithValue }) => {
try {
// TODO: Replace with actual API call
await new Promise(resolve => setTimeout(resolve, 1500));
// Mock patients data
const mockPatients: Patient[] = [
{
id: '1',
mrn: 'MRN001',
firstName: 'John',
lastName: 'Doe',
dateOfBirth: new Date('1985-03-15'),
gender: 'MALE',
age: 38,
bedNumber: 'A1',
roomNumber: '101',
admissionDate: new Date('2024-01-15'),
status: 'ACTIVE',
priority: 'CRITICAL',
department: 'Emergency',
attendingPhysician: 'Dr. Smith',
allergies: [
{
id: '1',
name: 'Penicillin',
severity: 'SEVERE',
reaction: 'Anaphylaxis',
},
],
medications: [
{
id: '1',
name: 'Morphine',
dosage: '2mg',
frequency: 'Every 4 hours',
route: 'IV',
startDate: new Date(),
status: 'ACTIVE',
prescribedBy: 'Dr. Smith',
},
],
vitalSigns: {
bloodPressure: { systolic: 140, diastolic: 90, timestamp: new Date() },
heartRate: { value: 95, timestamp: new Date() },
temperature: { value: 37.2, timestamp: new Date() },
respiratoryRate: { value: 18, timestamp: new Date() },
oxygenSaturation: { value: 98, timestamp: new Date() },
// Make actual API call to fetch medical cases
const response :any = await patientAPI.getPatients(token);
console.log('patients response',response)
if (response.ok && response.data&&response.data.success) {
// Add random case types to each patient record
const caseTypes: Array<'Critical' | 'Emergency' | 'Routine'> = ['Critical', 'Emergency', 'Routine'];
const patientsWithTypes = response.data.data.map((patient: any) => ({
...patient,
type: caseTypes[Math.floor(Math.random() * caseTypes.length)]
}));
console.log('patients with random types', patientsWithTypes);
return patientsWithTypes as MedicalCase[];
} else {
// Fallback to mock data for development
const mockPatients: MedicalCase[] = [
{
id: 1,
patientdetails: JSON.stringify({
patientdetails: {
Date: '2024-01-15',
Name: 'John Doe',
PatID: 'MRN001',
PatAge: '38',
PatSex: 'M',
Status: 'Active',
InstName: 'City General Hospital',
Modality: 'CT',
ReportStatus: 'Pending'
}
}),
series: JSON.stringify([
{
Path: ['/dicom/series1'],
SerDes: 'Chest CT',
ViePos: 'Supine',
pngpath: '/images/ct_chest_1.png',
SeriesNum: '1',
ImgTotalinSeries: '50'
}
]),
created_at: '2024-01-15T10:30:00Z',
updated_at: '2024-01-15T11:45:00Z',
series_id: 'series_001',
type: 'Critical'
},
medicalHistory: [],
currentDiagnosis: 'Chest pain, rule out MI',
lastUpdated: new Date(),
},
{
id: '2',
mrn: 'MRN002',
firstName: 'Jane',
lastName: 'Smith',
dateOfBirth: new Date('1990-07-22'),
gender: 'FEMALE',
age: 33,
bedNumber: 'B2',
roomNumber: '102',
admissionDate: new Date('2024-01-15'),
status: 'ACTIVE',
priority: 'HIGH',
department: 'Trauma',
attendingPhysician: 'Dr. Johnson',
allergies: [],
medications: [],
vitalSigns: {
bloodPressure: { systolic: 120, diastolic: 80, timestamp: new Date() },
heartRate: { value: 88, timestamp: new Date() },
temperature: { value: 36.8, timestamp: new Date() },
respiratoryRate: { value: 16, timestamp: new Date() },
oxygenSaturation: { value: 99, timestamp: new Date() },
{
id: 2,
patientdetails: JSON.stringify({
patientdetails: {
Date: '2024-01-15',
Name: 'Jane Smith',
PatID: 'MRN002',
PatAge: '33',
PatSex: 'F',
Status: 'Active',
InstName: 'Memorial Medical Center',
Modality: 'MR',
ReportStatus: 'Completed'
}
}),
series: JSON.stringify([
{
Path: ['/dicom/series2'],
SerDes: 'Brain MRI',
ViePos: 'Supine',
pngpath: '/images/mri_brain_1.png',
SeriesNum: '2',
ImgTotalinSeries: '120'
}
]),
created_at: '2024-01-15T09:15:00Z',
updated_at: '2024-01-15T10:30:00Z',
series_id: 'series_002',
type: 'Routine'
},
medicalHistory: [],
currentDiagnosis: 'Multiple trauma from MVA',
lastUpdated: new Date(),
},
];
return mockPatients;
} catch (error) {
return rejectWithValue('Failed to fetch patients.');
];
return mockPatients;
}
} catch (error: any) {
console.error('Fetch patients error:', error);
return rejectWithValue(error.message || 'Failed to fetch patients.');
}
}
);
@ -124,54 +125,38 @@ export const fetchPatientDetails = createAsyncThunk(
async (patientId: string, { rejectWithValue }) => {
try {
// TODO: Replace with actual API call
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
// Mock patient details (same as above but with more detailed info)
const mockPatient: Patient = {
id: patientId,
mrn: `MRN${patientId.padStart(3, '0')}`,
firstName: 'John',
lastName: 'Doe',
dateOfBirth: new Date('1985-03-15'),
gender: 'MALE',
age: 38,
bedNumber: 'A1',
roomNumber: '101',
admissionDate: new Date('2024-01-15'),
status: 'ACTIVE',
priority: 'CRITICAL',
department: 'Emergency',
attendingPhysician: 'Dr. Smith',
allergies: [
// Mock patient details for specific patient
const mockPatient: MedicalCase = {
id: parseInt(patientId),
patientdetails: JSON.stringify({
patientdetails: {
Date: '2024-01-15',
Name: 'John Doe',
PatID: `MRN${patientId.padStart(3, '0')}`,
PatAge: '38',
PatSex: 'M',
Status: 'Active',
InstName: 'City General Hospital',
Modality: 'CT',
ReportStatus: 'Pending'
}
}),
series: JSON.stringify([
{
id: '1',
name: 'Penicillin',
severity: 'SEVERE',
reaction: 'Anaphylaxis',
},
],
medications: [
{
id: '1',
name: 'Morphine',
dosage: '2mg',
frequency: 'Every 4 hours',
route: 'IV',
startDate: new Date(),
status: 'ACTIVE',
prescribedBy: 'Dr. Smith',
},
],
vitalSigns: {
bloodPressure: { systolic: 140, diastolic: 90, timestamp: new Date() },
heartRate: { value: 95, timestamp: new Date() },
temperature: { value: 37.2, timestamp: new Date() },
respiratoryRate: { value: 18, timestamp: new Date() },
oxygenSaturation: { value: 98, timestamp: new Date() },
},
medicalHistory: [],
currentDiagnosis: 'Chest pain, rule out MI',
lastUpdated: new Date(),
Path: [`/dicom/series${patientId}`],
SerDes: 'Chest CT',
ViePos: 'Supine',
pngpath: `/images/ct_chest_${patientId}.png`,
SeriesNum: patientId,
ImgTotalinSeries: '50'
}
]),
created_at: '2024-01-15T10:30:00Z',
updated_at: '2024-01-15T11:45:00Z',
series_id: `series_${patientId.padStart(3, '0')}`,
type: 'Critical'
};
return mockPatient;
@ -191,10 +176,10 @@ export const fetchPatientDetails = createAsyncThunk(
*/
export const updatePatient = createAsyncThunk(
'patientCare/updatePatient',
async (patientData: Partial<Patient> & { id: string }, { rejectWithValue }) => {
async (patientData: Partial<MedicalCase> & { id: number }, { rejectWithValue }) => {
try {
// TODO: Replace with actual API call
await new Promise(resolve => setTimeout(resolve, 800));
await new Promise((resolve) => setTimeout(resolve, 800));
return patientData;
} catch (error) {
return rejectWithValue('Failed to update patient.');
@ -234,7 +219,7 @@ const initialState: PatientCareState = {
// Search and filtering
searchQuery: '',
selectedFilter: 'all',
sortBy: 'priority',
sortBy: 'date',
sortOrder: 'desc',
// Pagination
@ -292,7 +277,7 @@ const patientCareSlice = createSlice({
*
* Purpose: Set patient filter
*/
setFilter: (state, action: PayloadAction<'all' | 'active' | 'discharged' | 'critical'>) => {
setFilter: (state, action: PayloadAction<'all' | 'Critical' | 'Routine' | 'Emergency'>) => {
state.selectedFilter = action.payload;
state.currentPage = 1; // Reset to first page when filtering
},
@ -302,7 +287,7 @@ const patientCareSlice = createSlice({
*
* Purpose: Set patient sort options
*/
setSort: (state, action: PayloadAction<{ by: string; order: 'asc' | 'desc' }>) => {
setSort: (state, action: PayloadAction<{ by: 'date' | 'name' | 'age'; order: 'asc' | 'desc' }>) => {
state.sortBy = action.payload.by;
state.sortOrder = action.payload.order;
},
@ -331,7 +316,7 @@ const patientCareSlice = createSlice({
*
* Purpose: Set the currently selected patient
*/
setCurrentPatient: (state, action: PayloadAction<Patient | null>) => {
setCurrentPatient: (state, action: PayloadAction<MedicalCase | null>) => {
state.currentPatient = action.payload;
},
@ -340,7 +325,7 @@ const patientCareSlice = createSlice({
*
* Purpose: Update a patient in the patients list
*/
updatePatientInList: (state, action: PayloadAction<Patient>) => {
updatePatientInList: (state, action: PayloadAction<MedicalCase>) => {
const index = state.patients.findIndex(patient => patient.id === action.payload.id);
if (index !== -1) {
state.patients[index] = action.payload;
@ -357,7 +342,7 @@ const patientCareSlice = createSlice({
*
* Purpose: Add a new patient to the list
*/
addPatient: (state, action: PayloadAction<Patient>) => {
addPatient: (state, action: PayloadAction<MedicalCase>) => {
state.patients.unshift(action.payload);
state.totalItems += 1;
},
@ -367,7 +352,7 @@ const patientCareSlice = createSlice({
*
* Purpose: Remove a patient from the list
*/
removePatient: (state, action: PayloadAction<string>) => {
removePatient: (state, action: PayloadAction<number>) => {
const index = state.patients.findIndex(patient => patient.id === action.payload);
if (index !== -1) {
state.patients.splice(index, 1);

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View File

@ -13,6 +13,7 @@ import { SettingsStackNavigator } from '../modules/Settings/navigation';
import { MainTabParamList } from './navigationTypes';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { ComingSoonScreen } from '../shared/components';
import { PatientsScreen } from '../modules/PatientCare';
// Create the bottom tab navigator
const Tab = createBottomTabNavigator<MainTabParamList>();
@ -83,7 +84,7 @@ export const MainTabNavigator: React.FC = () => {
{/* Patients Tab - Patient list and management */}
<Tab.Screen
name="Patients"
component={ComingSoonScreen} // TODO: Replace with actual PatientsScreen
component={PatientsScreen} // TODO: Replace with actual PatientsScreen
options={{
title: 'Patient List',
tabBarLabel: 'Patients',

View File

@ -5,29 +5,43 @@
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
export interface Patient {
id: string;
mrn: string; // Medical Record Number
firstName: string;
lastName: string;
dateOfBirth: Date;
gender: 'MALE' | 'FEMALE' | 'OTHER';
age: number;
bedNumber: string;
roomNumber: string;
admissionDate: Date;
status: PatientStatus;
priority: AlertPriority;
department: string;
attendingPhysician: string;
allergies: Allergy[];
medications: Medication[];
vitalSigns: VitalSigns;
medicalHistory: MedicalHistory[];
currentDiagnosis: string;
lastUpdated: Date;
// DICOM Patient Details Interface
export interface PatientDetails {
Date: string;
Name: string;
PatID: string;
PatAge: string;
PatSex: string;
Status: string;
InstName: string;
Modality: 'DX' | 'CT' | 'MR';
ReportStatus: string | null;
}
// DICOM Series Interface
export interface Series {
Path: string[];
SerDes: string;
ViePos: string | null;
pngpath: string;
SeriesNum: string;
ImgTotalinSeries: string;
}
// Medical Case Interface (Main Patient Record)
export interface MedicalCase {
id: number;
patientdetails: PatientDetails | string; // Can be object or JSON string
series: Series[] | string; // Can be array or JSON string
created_at: string;
updated_at: string;
series_id: string | null;
type: 'Critical' | 'Routine' | 'Emergency';
}
// Legacy Patient interface for backward compatibility
export interface Patient extends MedicalCase {}
export type PatientStatus =
| 'ACTIVE'
| 'PENDING'
@ -111,6 +125,35 @@ export interface ScanResult {
priority: AlertPriority;
}
export interface PatientCareState {
// Patients data (Medical Cases)
patients: MedicalCase[];
currentPatient: MedicalCase | null;
// Loading states
isLoading: boolean;
isRefreshing: boolean;
isLoadingPatientDetails: boolean;
// Error handling
error: string | null;
// Search and filtering
searchQuery: string;
selectedFilter: 'all' | 'Critical' | 'Routine' | 'Emergency';
sortBy: 'date' | 'name' | 'age';
sortOrder: 'asc' | 'desc';
// Pagination
currentPage: number;
itemsPerPage: number;
totalItems: number;
// Cache
lastUpdated: Date | null;
cacheExpiry: Date | null;
}
/*
* End of File: patient.ts
* Design & Developed by Tech4Biz Solutions