diff --git a/app/modules/PatientCare/components/EmptyState.tsx b/app/modules/PatientCare/components/EmptyState.tsx new file mode 100644 index 0000000..94e003c --- /dev/null +++ b/app/modules/PatientCare/components/EmptyState.tsx @@ -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 = ({ + title, + subtitle, + iconName = 'users', + onRetry, + retryText = 'Retry', +}) => { + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + {/* Icon */} + + + + + {/* Title */} + {title} + + {/* Subtitle */} + {subtitle} + + {/* Retry Button */} + {onRetry && ( + + + {retryText} + + )} + + ); +}; + +// ============================================================================ +// 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. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/components/FilterTabs.tsx b/app/modules/PatientCare/components/FilterTabs.tsx new file mode 100644 index 0000000..0d35f6f --- /dev/null +++ b/app/modules/PatientCare/components/FilterTabs.tsx @@ -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 = ({ + 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 ( + onFilterChange(tab.id)} + activeOpacity={0.7} + > + {/* Tab Icon */} + + + {/* Tab Content */} + + + {tab.label} + + + {/* Patient Count Badge */} + + + {patientCount} + + + + + {/* Critical Indicator */} + {tab.id === 'Critical' && patientCount > 0 && ( + + + + )} + + ); + }; + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + + {filterTabs.map(renderFilterTab)} + + + {/* Active Filter Indicator */} + + + + + ); +}; + +// ============================================================================ +// 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. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/components/LoadingState.tsx b/app/modules/PatientCare/components/LoadingState.tsx new file mode 100644 index 0000000..806ea7f --- /dev/null +++ b/app/modules/PatientCare/components/LoadingState.tsx @@ -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 = ({ + 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 = () => ( + + {/* Loading Animation */} + + {showIcon && ( + + + + )} + + + + {/* Loading Text */} + {title} + {subtitle} + + ); + + /** + * Render Small Loading State + * + * Purpose: Render compact loading state + */ + const renderSmallState = () => ( + + + {title} + + ); + + // ============================================================================ + // 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. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/components/PatientCard.tsx b/app/modules/PatientCare/components/PatientCard.tsx new file mode 100644 index 0000000..a1627b9 --- /dev/null +++ b/app/modules/PatientCare/components/PatientCard.tsx @@ -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 = ({ + 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 = () => ( + + + {patient.type} + + ); + + /** + * Render Emergency Button + * + * Purpose: Render emergency alert button for critical cases + */ + const renderEmergencyButton = () => { + if (patient.type !== 'Critical') { + return null; + } + + return ( + + + ALERT + + ); + }; + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + {/* Header Section */} + + + + {patientData.Name || 'Unknown Patient'} + + + ID: {patientData.PatID || 'N/A'} • {patientData.PatAge || 'N/A'}y • {patientData.PatSex || 'N/A'} + + + + {renderTypeBadge()} + {renderEmergencyButton()} + + + + {/* Medical Information Section */} + + + + Modality + + {patientData.Modality || 'N/A'} + + + + Status + + {patientData.Status || 'Unknown'} + + + + Report + + {patientData.ReportStatus || 'Pending'} + + + + + {/* Institution */} + + + + {patientData.InstName || 'Unknown Institution'} + + + + + {/* Series Information */} + + + + Series Information + + + {Array.isArray(series) ? series.length : 0} Series Available + + + + {/* Footer */} + + + {formatDate(patient.created_at)} + + + Case #{patient.id} + + + + + ); +}; + +// ============================================================================ +// 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. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/components/SearchBar.tsx b/app/modules/PatientCare/components/SearchBar.tsx new file mode 100644 index 0000000..e1d9889 --- /dev/null +++ b/app/modules/PatientCare/components/SearchBar.tsx @@ -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 = ({ + value, + onChangeText, + placeholder = 'Search patients...', + showFilter = false, + onFilterPress, +}) => { + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Handle Clear Search + * + * Purpose: Clear the search input + */ + const handleClear = () => { + onChangeText(''); + }; + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + + {/* Search Icon */} + + + {/* Search Input */} + + + {/* Clear Button */} + {value.length > 0 && ( + + + + )} + + + {/* Filter Button */} + {showFilter && ( + + + + )} + + ); +}; + +// ============================================================================ +// 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. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/components/index.ts b/app/modules/PatientCare/components/index.ts new file mode 100644 index 0000000..01a0340 --- /dev/null +++ b/app/modules/PatientCare/components/index.ts @@ -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. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/index.ts b/app/modules/PatientCare/index.ts new file mode 100644 index 0000000..8a53851 --- /dev/null +++ b/app/modules/PatientCare/index.ts @@ -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. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/redux/patientCareSelectors.ts b/app/modules/PatientCare/redux/patientCareSelectors.ts new file mode 100644 index 0000000..efdae2c --- /dev/null +++ b/app/modules/PatientCare/redux/patientCareSelectors.ts @@ -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. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/redux/patientCareSlice.ts b/app/modules/PatientCare/redux/patientCareSlice.ts index 6908a7f..08c28cb 100644 --- a/app/modules/PatientCare/redux/patientCareSlice.ts +++ b/app/modules/PatientCare/redux/patientCareSlice.ts @@ -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 & { id: string }, { rejectWithValue }) => { + async (patientData: Partial & { 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) => { + setCurrentPatient: (state, action: PayloadAction) => { state.currentPatient = action.payload; }, @@ -340,7 +325,7 @@ const patientCareSlice = createSlice({ * * Purpose: Update a patient in the patients list */ - updatePatientInList: (state, action: PayloadAction) => { + updatePatientInList: (state, action: PayloadAction) => { 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) => { + addPatient: (state, action: PayloadAction) => { 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) => { + removePatient: (state, action: PayloadAction) => { const index = state.patients.findIndex(patient => patient.id === action.payload); if (index !== -1) { state.patients.splice(index, 1); diff --git a/app/modules/PatientCare/screens/PatientsScreen.tsx b/app/modules/PatientCare/screens/PatientsScreen.tsx new file mode 100644 index 0000000..9500d28 --- /dev/null +++ b/app/modules/PatientCare/screens/PatientsScreen.tsx @@ -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 = ({ 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 }) => ( + handlePatientPress(item)} + onEmergencyPress={() => handleEmergencyAlert(item)} + /> + ); + + /** + * Render Empty State + * + * Purpose: Render empty state when no patients found + */ + const renderEmptyState = () => { + if (isLoading) return null; + + return ( + + ); + }; + + + + /** + * Render Loading State + * + * Purpose: Render loading state during initial fetch + */ + if (isLoading && patients.length === 0) { + return ( + + + + + ); + } + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + + + {/* Fixed Header */} + + + navigation.goBack()} + > + + + + Patients + Emergency Department + + + + + + + + + { + // TODO: Implement notifications screen + navigation.navigate('Notifications'); + }} + > + + {/* Notification badge */} + + 3 + + + + + + {/* Fixed Search and Filter Section */} + + {/* Search Bar */} + + setShowSortModal(true)} + /> + + + {/* Filter Tabs */} + + p.type === 'Critical').length, + Routine: patients.filter((p: MedicalCase) => p.type === 'Routine').length, + Emergency: patients.filter((p: MedicalCase) => p.type === 'Emergency').length, + }} + /> + + + {/* Results Summary */} + + + + + {filteredPatients.length} patient{filteredPatients.length !== 1 ? 's' : ''} found + + + + + + Sorted by {sortBy} + + + + + + {/* Scrollable Patient List Only */} + index.toString()} + ListEmptyComponent={renderEmptyState} + contentContainerStyle={[ + styles.listContent, + filteredPatients.length === 0 && styles.emptyListContent + ]} + showsVerticalScrollIndicator={false} + refreshControl={ + + } + // Performance optimizations + removeClippedSubviews={true} + maxToRenderPerBatch={10} + windowSize={10} + initialNumToRender={8} + getItemLayout={(data, index) => ({ + length: 120, // Approximate height of PatientCard + offset: 120 * index, + index, + })} + /> + + ); +}; + +// ============================================================================ +// 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. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/screens/index.ts b/app/modules/PatientCare/screens/index.ts new file mode 100644 index 0000000..582cd0c --- /dev/null +++ b/app/modules/PatientCare/screens/index.ts @@ -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. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/services/index.ts b/app/modules/PatientCare/services/index.ts new file mode 100644 index 0000000..520a274 --- /dev/null +++ b/app/modules/PatientCare/services/index.ts @@ -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. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/services/patientAPI.ts b/app/modules/PatientCare/services/patientAPI.ts new file mode 100644 index 0000000..0f86db4 --- /dev/null +++ b/app/modules/PatientCare/services/patientAPI.ts @@ -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. + */ \ No newline at end of file diff --git a/app/navigation/MainTabNavigator.tsx b/app/navigation/MainTabNavigator.tsx index 53ced11..a199b1a 100644 --- a/app/navigation/MainTabNavigator.tsx +++ b/app/navigation/MainTabNavigator.tsx @@ -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(); @@ -83,7 +84,7 @@ export const MainTabNavigator: React.FC = () => { {/* Patients Tab - Patient list and management */}