576 lines
17 KiB
TypeScript
576 lines
17 KiB
TypeScript
/*
|
|
* File: DashboardScreen.tsx
|
|
* Description: Sample dashboard screen using Clinical Blue Interface and shared components
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/
|
|
|
|
import React, { useState, useMemo } from 'react';
|
|
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
|
|
import { Button } from '../../../../shared/src/components/Button';
|
|
import { Card, InfoCard } from '../../../../shared/src/components/Card';
|
|
import { SearchInput } from '../../../../shared/src/components/Input';
|
|
import { Modal } from '../../../../shared/src/components/Modal';
|
|
import { CustomIcon, IconButton } from '../../../../shared/src/components/Icons';
|
|
import { CustomHeader } from '../../../../shared/src/components/Header';
|
|
import { Colors, Spacing, Typography } from '../../../../shared/src/theme';
|
|
import { useSelector } from 'react-redux';
|
|
import { selectPatients } from '../redux';
|
|
|
|
interface PatientDetails {
|
|
Date: string;
|
|
Name: string;
|
|
PatID: string;
|
|
PatAge: string;
|
|
PatSex: string;
|
|
Status: string;
|
|
InstName: string;
|
|
Modality: 'DX' | 'CT' | 'MR';
|
|
ReportStatus: string | null;
|
|
}
|
|
|
|
interface Series {
|
|
Path: string[];
|
|
SerDes: string;
|
|
ViePos: string | null;
|
|
pngpath: string;
|
|
SeriesNum: string;
|
|
ImgTotalinSeries: string;
|
|
}
|
|
|
|
export interface MedicalCase {
|
|
id: number;
|
|
patientdetails: PatientDetails;
|
|
series: Series[];
|
|
created_at: string;
|
|
updated_at: string;
|
|
series_id: string | null;
|
|
type: 'Critical' | 'Routine' | 'Emergency';
|
|
}
|
|
|
|
interface DashBoardDetailProps {
|
|
navigation: any;
|
|
route: {
|
|
params: {
|
|
caseType: 'Critical' | 'Routine' | 'Emergency';
|
|
};
|
|
};
|
|
}
|
|
|
|
/**
|
|
* DashBoardDetail - Shows detailed list of patients for a specific case type
|
|
*/
|
|
const DashBoardDetail: React.FC<DashBoardDetailProps> = ({ navigation, route }) => {
|
|
const [search, setSearch] = useState('');
|
|
const [selectedPatient, setSelectedPatient] = useState<MedicalCase | null>(null);
|
|
const [modalVisible, setModalVisible] = useState(false);
|
|
const [sortBy, setSortBy] = useState<'date' | 'name' | 'age'>('date');
|
|
|
|
const caseType = route.params?.caseType || 'Critical';
|
|
const patientData: MedicalCase[] = useSelector(selectPatients) || [];
|
|
|
|
// Filter patients by case type and search
|
|
const filteredPatients = useMemo(() => {
|
|
let filtered = patientData.filter(case_ => case_.type === caseType);
|
|
|
|
if (search.trim()) {
|
|
filtered = filtered.filter(case_ =>
|
|
case_.patientdetails.Name.toLowerCase().includes(search.toLowerCase()) ||
|
|
case_.patientdetails.PatID.toLowerCase().includes(search.toLowerCase()) ||
|
|
case_.patientdetails.InstName.toLowerCase().includes(search.toLowerCase())
|
|
);
|
|
}
|
|
|
|
// Sort patients
|
|
return filtered.sort((a, b) => {
|
|
switch (sortBy) {
|
|
case 'name':
|
|
return a.patientdetails.Name.localeCompare(b.patientdetails.Name);
|
|
case 'age':
|
|
return parseInt(b.patientdetails.PatAge) - parseInt(a.patientdetails.PatAge);
|
|
case 'date':
|
|
default:
|
|
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
|
}
|
|
});
|
|
}, [patientData, caseType, search, sortBy]);
|
|
|
|
const getCaseTypeConfig = (type: string) => {
|
|
switch (type) {
|
|
case 'Critical':
|
|
return {
|
|
color: Colors.error,
|
|
icon: 'alert-circle',
|
|
bgColor: '#FFF5F5' // Light red background
|
|
};
|
|
case 'Emergency':
|
|
return {
|
|
color: '#FF8C00',
|
|
icon: 'alert',
|
|
bgColor: '#FFF8E1' // Light orange background
|
|
};
|
|
case 'Routine':
|
|
return {
|
|
color: Colors.success,
|
|
icon: 'check-circle',
|
|
bgColor: '#F0FFF4' // Light green background
|
|
};
|
|
default:
|
|
return {
|
|
color: Colors.primary,
|
|
icon: 'info',
|
|
bgColor: Colors.background
|
|
};
|
|
}
|
|
};
|
|
|
|
const typeConfig = getCaseTypeConfig(caseType);
|
|
|
|
const handlePatientPress = (patient: MedicalCase) => {
|
|
setSelectedPatient(patient);
|
|
setModalVisible(true);
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
|
|
const getModalityColor = (modality: string) => {
|
|
switch (modality) {
|
|
case 'CT': return '#4A90E2';
|
|
case 'MR': return '#7B68EE';
|
|
case 'DX': return '#50C878';
|
|
default: return Colors.textSecondary;
|
|
}
|
|
};
|
|
|
|
const renderPatientCard = (patient: MedicalCase) => (
|
|
<TouchableOpacity
|
|
key={patient.id}
|
|
onPress={() => handlePatientPress(patient)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Card style={[styles.patientCard, { borderLeftColor: typeConfig.color }]}>
|
|
<View style={styles.cardHeader}>
|
|
<View style={styles.patientInfo}>
|
|
<Text style={styles.patientName}>
|
|
{patient.patientdetails.Name}
|
|
</Text>
|
|
<Text style={styles.patientId}>
|
|
ID: {patient.patientdetails.PatID}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.urgencyBadge}>
|
|
<CustomIcon
|
|
name={typeConfig.icon}
|
|
color={typeConfig.color}
|
|
size={16}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.cardBody}>
|
|
<View style={styles.infoRow}>
|
|
<View style={styles.infoItem}>
|
|
<Text style={styles.infoLabel}>Age</Text>
|
|
<Text style={styles.infoValue}>{patient.patientdetails.PatAge}</Text>
|
|
</View>
|
|
<View style={styles.infoItem}>
|
|
<Text style={styles.infoLabel}>Sex</Text>
|
|
<Text style={styles.infoValue}>{patient.patientdetails.PatSex}</Text>
|
|
</View>
|
|
<View style={styles.infoItem}>
|
|
<Text style={styles.infoLabel}>Modality</Text>
|
|
<Text style={[styles.infoValue, styles.modalityText, {
|
|
color: getModalityColor(patient.patientdetails.Modality?.substring(1))
|
|
}]}>
|
|
{patient.patientdetails.Modality?.substring(1)}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.institutionRow}>
|
|
<CustomIcon name="hospital-building" color={Colors.textPrimary} size={14} />
|
|
<Text style={styles.institutionText}>
|
|
{patient.patientdetails.InstName}
|
|
</Text>
|
|
</View>
|
|
|
|
<View style={styles.bottomRow}>
|
|
<Text style={styles.dateText}>
|
|
{formatDate(patient.created_at)}
|
|
</Text>
|
|
<Text style={[styles.statusText, {
|
|
color: patient.patientdetails.Status === 'Active' ? Colors.success : Colors.textSecondary
|
|
}]}>
|
|
{patient.patientdetails.Status}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.cardFooter}>
|
|
<Text style={styles.seriesText}>
|
|
{patient.series.length} Series Available
|
|
</Text>
|
|
<IconButton
|
|
name="chevron-right"
|
|
color={typeConfig.color}
|
|
size={20}
|
|
onPress={() => handlePatientPress(patient)}
|
|
/>
|
|
</View>
|
|
</Card>
|
|
</TouchableOpacity>
|
|
);
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<CustomHeader
|
|
title={`${caseType} Cases`}
|
|
showBackButton={true}
|
|
// onBackPress={() => navigation.goBack()}
|
|
/>
|
|
|
|
<ScrollView contentContainerStyle={styles.content}>
|
|
{/* Summary Card */}
|
|
<InfoCard style={[styles.summaryCard, { backgroundColor: typeConfig.bgColor }]}>
|
|
<View style={styles.summaryHeader}>
|
|
<CustomIcon name={typeConfig.icon} color={typeConfig.color} size={28} />
|
|
<View style={styles.summaryText}>
|
|
<Text style={styles.summaryTitle}>
|
|
{caseType} Cases
|
|
</Text>
|
|
<Text style={styles.summaryCount}>
|
|
{filteredPatients.length} Patient{filteredPatients.length !== 1 ? 's' : ''}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</InfoCard>
|
|
|
|
{/* Search and Sort */}
|
|
<View style={styles.controlsContainer}>
|
|
<SearchInput
|
|
placeholder={`Search ${caseType.toLowerCase()} cases...`}
|
|
value={search}
|
|
onChangeText={setSearch}
|
|
containerStyle={styles.searchInput}
|
|
/>
|
|
|
|
<View style={styles.sortContainer}>
|
|
<Text style={styles.sortLabel}>Sort by:</Text>
|
|
<View style={styles.sortButtons}>
|
|
{(['date', 'name', 'age'] as const).map(option => (
|
|
<TouchableOpacity
|
|
key={option}
|
|
style={[
|
|
styles.sortButton,
|
|
sortBy === option && { backgroundColor: typeConfig.color }
|
|
]}
|
|
onPress={() => setSortBy(option)}
|
|
>
|
|
<Text style={[
|
|
styles.sortButtonText,
|
|
sortBy === option && { color: '#FFFFFF' }
|
|
]}>
|
|
{option.charAt(0).toUpperCase() + option.slice(1)}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Patient List */}
|
|
{filteredPatients.length > 0 ? (
|
|
filteredPatients.map(renderPatientCard)
|
|
) : (
|
|
<Card style={styles.emptyCard}>
|
|
<CustomIcon name="inbox" color={Colors.textSecondary} size={48} />
|
|
<Text style={styles.emptyTitle}>
|
|
No {caseType.toLowerCase()} cases found
|
|
</Text>
|
|
<Text style={styles.emptySubtitle}>
|
|
{search ? 'Try adjusting your search terms' : 'All clear for now!'}
|
|
</Text>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Patient Detail Modal */}
|
|
<Modal
|
|
visible={modalVisible}
|
|
onRequestClose={() => setModalVisible(false)}
|
|
>
|
|
{selectedPatient && (
|
|
<View style={styles.modalContent}>
|
|
<Text style={styles.modalTitle}>Patient Details</Text>
|
|
|
|
<View style={styles.modalSection}>
|
|
<Text style={styles.modalSectionTitle}>Basic Information</Text>
|
|
<Text style={styles.modalText}>Name: {selectedPatient.patientdetails.Name}</Text>
|
|
<Text style={styles.modalText}>ID: {selectedPatient.patientdetails.PatID}</Text>
|
|
<Text style={styles.modalText}>Age: {selectedPatient.patientdetails.PatAge}</Text>
|
|
<Text style={styles.modalText}>Sex: {selectedPatient.patientdetails.PatSex}</Text>
|
|
<Text style={styles.modalText}>Status: {selectedPatient.patientdetails.Status}</Text>
|
|
</View>
|
|
|
|
<View style={styles.modalSection}>
|
|
<Text style={styles.modalSectionTitle}>Medical Information</Text>
|
|
<Text style={styles.modalText}>Modality: {selectedPatient.patientdetails.Modality}</Text>
|
|
<Text style={styles.modalText}>Institution: {selectedPatient.patientdetails.InstName}</Text>
|
|
<Text style={styles.modalText}>Report Status: {selectedPatient.patientdetails.ReportStatus || 'Pending'}</Text>
|
|
<Text style={styles.modalText}>Case Type: {selectedPatient.type}</Text>
|
|
</View>
|
|
|
|
<View style={styles.modalSection}>
|
|
<Text style={styles.modalSectionTitle}>Series Information</Text>
|
|
<Text style={styles.modalText}>Available Series: {selectedPatient.series.length}</Text>
|
|
<Text style={styles.modalText}>Created: {formatDate(selectedPatient.created_at)}</Text>
|
|
<Text style={styles.modalText}>Updated: {formatDate(selectedPatient.updated_at)}</Text>
|
|
</View>
|
|
|
|
<View style={styles.modalButtons}>
|
|
<Button
|
|
title="View Images"
|
|
onPress={() => {
|
|
// Navigate to image viewer
|
|
setModalVisible(false);
|
|
// navigation.navigate('ImageViewer', { patientId: selectedPatient.id });
|
|
}}
|
|
style={[styles.modalButton, { backgroundColor: typeConfig.color }]}
|
|
/>
|
|
<Button
|
|
title="Close"
|
|
onPress={() => setModalVisible(false)}
|
|
style={styles.modalButton}
|
|
/>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</Modal>
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: Colors.background,
|
|
},
|
|
content: {
|
|
padding: Spacing.lg,
|
|
},
|
|
summaryCard: {
|
|
marginBottom: Spacing.lg,
|
|
},
|
|
summaryHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
summaryText: {
|
|
marginLeft: Spacing.md,
|
|
flex: 1,
|
|
},
|
|
summaryTitle: {
|
|
fontFamily: Typography.fontFamily.bold,
|
|
fontSize: Typography.fontSize.lg,
|
|
color: Colors.textPrimary,
|
|
},
|
|
summaryCount: {
|
|
fontFamily: Typography.fontFamily.regular,
|
|
fontSize: Typography.fontSize.md,
|
|
color: Colors.textSecondary,
|
|
marginTop: Spacing.xs,
|
|
},
|
|
controlsContainer: {
|
|
marginBottom: Spacing.lg,
|
|
},
|
|
searchInput: {
|
|
marginBottom: Spacing.md,
|
|
},
|
|
sortContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
},
|
|
sortLabel: {
|
|
fontFamily: Typography.fontFamily.regular,
|
|
fontSize: Typography.fontSize.sm,
|
|
color: Colors.textSecondary,
|
|
},
|
|
sortButtons: {
|
|
flexDirection: 'row',
|
|
},
|
|
sortButton: {
|
|
paddingHorizontal: Spacing.sm,
|
|
paddingVertical: Spacing.xs,
|
|
backgroundColor: Colors.cardBackground,
|
|
borderRadius: 16,
|
|
marginLeft: Spacing.xs,
|
|
},
|
|
sortButtonText: {
|
|
fontFamily: Typography.fontFamily.regular,
|
|
fontSize: Typography.fontSize.sm,
|
|
color: Colors.textSecondary,
|
|
},
|
|
patientCard: {
|
|
marginBottom: Spacing.md,
|
|
borderLeftWidth: 4,
|
|
},
|
|
cardHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-start',
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
patientInfo: {
|
|
flex: 1,
|
|
},
|
|
patientName: {
|
|
fontFamily: Typography.fontFamily.bold,
|
|
fontSize: Typography.fontSize.md,
|
|
color: Colors.textPrimary,
|
|
},
|
|
patientId: {
|
|
fontFamily: Typography.fontFamily.regular,
|
|
fontSize: Typography.fontSize.sm,
|
|
color: Colors.textSecondary,
|
|
marginTop: Spacing.xs,
|
|
},
|
|
urgencyBadge: {
|
|
padding: Spacing.xs,
|
|
borderRadius: 12,
|
|
backgroundColor: 'rgba(0,0,0,0.05)',
|
|
},
|
|
cardBody: {
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
infoRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
infoItem: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
},
|
|
infoLabel: {
|
|
fontFamily: Typography.fontFamily.regular,
|
|
fontSize: Typography.fontSize.xs,
|
|
color: Colors.textSecondary,
|
|
marginBottom: Spacing.xs,
|
|
},
|
|
infoValue: {
|
|
fontFamily: Typography.fontFamily.bold,
|
|
fontSize: Typography.fontSize.sm,
|
|
color: Colors.textPrimary,
|
|
},
|
|
modalityText: {
|
|
fontWeight: 'bold',
|
|
},
|
|
institutionRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
institutionText: {
|
|
fontFamily: Typography.fontFamily.regular,
|
|
fontSize: Typography.fontSize.sm,
|
|
color: Colors.textSecondary,
|
|
marginLeft: Spacing.xs,
|
|
flex: 1,
|
|
},
|
|
bottomRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
},
|
|
dateText: {
|
|
fontFamily: Typography.fontFamily.regular,
|
|
fontSize: Typography.fontSize.xs,
|
|
color: Colors.textSecondary,
|
|
},
|
|
statusText: {
|
|
fontFamily: Typography.fontFamily.bold,
|
|
fontSize: Typography.fontSize.xs,
|
|
},
|
|
cardFooter: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
paddingTop: Spacing.sm,
|
|
borderTopWidth: 1,
|
|
borderTopColor: '#E5E5E5',
|
|
},
|
|
seriesText: {
|
|
fontFamily: Typography.fontFamily.regular,
|
|
fontSize: Typography.fontSize.sm,
|
|
color: Colors.textSecondary,
|
|
},
|
|
emptyCard: {
|
|
alignItems: 'center',
|
|
padding: Spacing.xl,
|
|
marginTop: Spacing.xl,
|
|
},
|
|
emptyTitle: {
|
|
fontFamily: Typography.fontFamily.bold,
|
|
fontSize: Typography.fontSize.lg,
|
|
color: Colors.textPrimary,
|
|
marginTop: Spacing.md,
|
|
textAlign: 'center',
|
|
},
|
|
emptySubtitle: {
|
|
fontFamily: Typography.fontFamily.regular,
|
|
fontSize: Typography.fontSize.md,
|
|
color: Colors.textSecondary,
|
|
marginTop: Spacing.xs,
|
|
textAlign: 'center',
|
|
},
|
|
modalContent: {
|
|
maxHeight: '80%',
|
|
},
|
|
modalTitle: {
|
|
fontFamily: Typography.fontFamily.bold,
|
|
fontSize: Typography.fontSize.lg,
|
|
color: Colors.primary,
|
|
marginBottom: Spacing.lg,
|
|
textAlign: 'center',
|
|
},
|
|
modalSection: {
|
|
marginBottom: Spacing.md,
|
|
},
|
|
modalSectionTitle: {
|
|
fontFamily: Typography.fontFamily.bold,
|
|
fontSize: Typography.fontSize.md,
|
|
color: Colors.textPrimary,
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
modalText: {
|
|
fontFamily: Typography.fontFamily.regular,
|
|
fontSize: Typography.fontSize.sm,
|
|
color: Colors.textPrimary,
|
|
marginBottom: Spacing.xs,
|
|
},
|
|
modalButtons: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-around',
|
|
marginTop: Spacing.lg,
|
|
},
|
|
modalButton: {
|
|
flex: 0.4,
|
|
},
|
|
});
|
|
|
|
export default DashBoardDetail;
|
|
|
|
/*
|
|
* End of File: DashboardScreen.tsx
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/
|