420 lines
12 KiB
TypeScript
420 lines
12 KiB
TypeScript
/*
|
|
* File: DashboardScreen.tsx
|
|
* Description: Main dashboard screen with a custom header and themed components.
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/
|
|
|
|
import React, { useEffect, useState, useMemo } from 'react';
|
|
import { View, Text, StyleSheet, ScrollView } from 'react-native';
|
|
import { Button } from '../../../../shared/src/components/Button';
|
|
import { Card, InfoCard } from '../../../../shared/src/components/Card';
|
|
import { TextInput, SearchInput } from '../../../../shared/src/components/Input';
|
|
import { Modal, ConfirmModal, AlertModal } from '../../../../shared/src/components/Modal';
|
|
import { Spinner, LoadingOverlay } from '../../../../shared/src/components/Loading';
|
|
import { CustomIcon, IconButton } from '../../../../shared/src/components/Icons';
|
|
import { CustomHeader } from '../../../../shared/src/components/Header';
|
|
import { Colors, Spacing, Typography } from '../../../../shared/src/theme';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { fetchPatients, 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 CaseStats {
|
|
total: number;
|
|
modalityCounts: {
|
|
DX: number;
|
|
CT: number;
|
|
MR: number;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* DashboardScreen - The primary landing screen after login, featuring a custom header,
|
|
* case summaries, and navigation to other parts of the app.
|
|
*/
|
|
const DashboardScreen: React.FC<{ navigation: any }> = ({ navigation }) => {
|
|
const [modalVisible, setModalVisible] = useState(false);
|
|
const [confirmVisible, setConfirmVisible] = useState(false);
|
|
const [alertVisible, setAlertVisible] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
const [search, setSearch] = useState('');
|
|
const [input, setInput] = useState('');
|
|
const [selectedCase, setSelectedCase] = useState<MedicalCase | null>(null);
|
|
|
|
const dispatch = useDispatch();
|
|
const user = useSelector((state: any) => state.auth.user);
|
|
const patientData: MedicalCase[] = useSelector(selectPatients) || [];
|
|
|
|
useEffect(() => {
|
|
//@ts-ignore
|
|
dispatch(fetchPatients(user?.access_token));
|
|
}, []);
|
|
|
|
// Filter cases based on search
|
|
const filteredCases = useMemo(() => {
|
|
if (!search.trim()) return patientData;
|
|
|
|
return patientData.filter(case_ =>
|
|
case_.patientdetails.Name.toLowerCase().includes(search.toLowerCase()) ||
|
|
case_.patientdetails.PatID.toLowerCase().includes(search.toLowerCase()) ||
|
|
case_.patientdetails.InstName.toLowerCase().includes(search.toLowerCase())
|
|
);
|
|
}, [patientData, search]);
|
|
|
|
// Calculate statistics for each case type
|
|
const caseStats = useMemo(() => {
|
|
const stats: Record<string, CaseStats> = {
|
|
Critical: { total: 0, modalityCounts: { DX: 0, CT: 0, MR: 0 } },
|
|
Routine: { total: 0, modalityCounts: { DX: 0, CT: 0, MR: 0 } },
|
|
Emergency: { total: 0, modalityCounts: { DX: 0, CT: 0, MR: 0 } }
|
|
};
|
|
|
|
filteredCases.forEach(case_ => {
|
|
const type = case_.type;
|
|
const modality = case_.patientdetails?.Modality;
|
|
|
|
stats[type].total += 1;
|
|
stats[type].modalityCounts[modality?.substring(1)] += 1;
|
|
});
|
|
|
|
return stats;
|
|
}, [filteredCases]);
|
|
|
|
const getCaseIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'Critical':
|
|
return { name: 'alert', color: Colors.error };
|
|
case 'Emergency':
|
|
return { name: 'alert', color: '#FF8C00' }; // Orange color for emergency
|
|
case 'Routine':
|
|
return { name: 'check-circle', color: Colors.success };
|
|
default:
|
|
return { name: 'info', color: Colors.primary };
|
|
}
|
|
};
|
|
|
|
const getCaseColor = (type: string) => {
|
|
switch (type) {
|
|
case 'Critical':
|
|
return Colors.error;
|
|
case 'Emergency':
|
|
return '#FF8C00'; // Orange
|
|
case 'Routine':
|
|
return Colors.success;
|
|
default:
|
|
return Colors.textSecondary;
|
|
}
|
|
};
|
|
|
|
const handleCasePress = (type: string) => {
|
|
const casesOfType = filteredCases.filter(case_ => case_.type === type);
|
|
if (casesOfType.length > 0) {
|
|
navigation.navigate('DashboardDetailScreen',{caseType:type})
|
|
// setSelectedCase(casesOfType[0]);
|
|
// setModalVisible(true);
|
|
}
|
|
};
|
|
|
|
const renderCaseCard = (type: 'Critical' | 'Routine' | 'Emergency') => {
|
|
const stats = caseStats[type];
|
|
const icon = getCaseIcon(type);
|
|
const color = getCaseColor(type);
|
|
|
|
if (stats.total === 0) return null; // Don't render card if no cases of this type
|
|
return (
|
|
<Card key={type} style={styles.caseCard}>
|
|
<View style={styles.cardHeader}>
|
|
<View style={styles.titleRow}>
|
|
<CustomIcon name={icon.name} color={icon.color} size={24} />
|
|
<Text style={styles.cardTitle}>{type} Cases</Text>
|
|
</View>
|
|
<Text style={[styles.totalCount, { color }]}>
|
|
{stats.total} Total
|
|
</Text>
|
|
</View>
|
|
|
|
<View style={styles.modalityContainer}>
|
|
<Text style={styles.modalityLabel}>By Modality:</Text>
|
|
<View style={styles.modalityRow}>
|
|
{(['DX', 'CT', 'MR'] as const).map(modality => (
|
|
<View key={modality} style={styles.modalityItem}>
|
|
<Text style={styles.modalityType}>{modality}</Text>
|
|
<Text style={[styles.modalityCount, { color }]}>
|
|
{stats.modalityCounts[modality]}
|
|
</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
|
|
<Button
|
|
title={`View ${type} Cases`}
|
|
onPress={() => handleCasePress(type)}
|
|
style={[styles.button, { backgroundColor: color }]}
|
|
/>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<CustomHeader
|
|
title="Dashboard"
|
|
showBackButton={false}
|
|
/>
|
|
<ScrollView contentContainerStyle={styles.content}>
|
|
{/* <SearchInput
|
|
placeholder="Search cases..."
|
|
value={search}
|
|
onChangeText={setSearch}
|
|
containerStyle={styles.search}
|
|
/> */}
|
|
|
|
<InfoCard>
|
|
<Text style={styles.infoTitle}>Welcome, Dr. {user?.display_name}</Text>
|
|
<Text style={styles.infoText}>
|
|
On-Call Status: <Text style={styles.active}>ACTIVE</Text>
|
|
</Text>
|
|
<Text style={styles.infoText}>
|
|
Total Cases: <Text style={styles.active}>{filteredCases.length}</Text>
|
|
</Text>
|
|
</InfoCard>
|
|
|
|
{/* Dynamic Case Cards */}
|
|
{(['Critical', 'Emergency', 'Routine'] as const).map(type =>
|
|
renderCaseCard(type)
|
|
)}
|
|
|
|
{filteredCases.length === 0 && (
|
|
<Card>
|
|
<Text style={styles.noDataText}>
|
|
{search ? 'No cases found matching your search.' : 'No cases available.'}
|
|
</Text>
|
|
</Card>
|
|
)}
|
|
|
|
<TextInput
|
|
placeholder="Add note..."
|
|
value={input}
|
|
onChangeText={setInput}
|
|
style={styles.input}
|
|
/>
|
|
|
|
{/* Modals */}
|
|
<Modal
|
|
visible={modalVisible}
|
|
onRequestClose={() => setModalVisible(false)}>
|
|
{selectedCase && (
|
|
<>
|
|
<Text style={styles.modalTitle}>
|
|
{selectedCase.type} Case Details
|
|
</Text>
|
|
<View style={styles.modalContent}>
|
|
<Text style={styles.modalText}>
|
|
Patient: {selectedCase.patientdetails.Name}
|
|
</Text>
|
|
<Text style={styles.modalText}>
|
|
ID: {selectedCase.patientdetails.PatID}
|
|
</Text>
|
|
<Text style={styles.modalText}>
|
|
Age: {selectedCase.patientdetails.PatAge}
|
|
</Text>
|
|
<Text style={styles.modalText}>
|
|
Sex: {selectedCase.patientdetails.PatSex}
|
|
</Text>
|
|
<Text style={styles.modalText}>
|
|
Modality: {selectedCase.patientdetails.Modality}
|
|
</Text>
|
|
<Text style={styles.modalText}>
|
|
Institution: {selectedCase.patientdetails.InstName}
|
|
</Text>
|
|
<Text style={styles.modalText}>
|
|
Status: {selectedCase.patientdetails.Status}
|
|
</Text>
|
|
<Text style={styles.modalText}>
|
|
Series: {selectedCase.series.length} available
|
|
</Text>
|
|
</View>
|
|
<Button
|
|
title="Close"
|
|
onPress={() => {
|
|
setModalVisible(false);
|
|
setSelectedCase(null);
|
|
}}
|
|
style={styles.button}
|
|
/>
|
|
</>
|
|
)}
|
|
</Modal>
|
|
|
|
<ConfirmModal
|
|
visible={confirmVisible}
|
|
title="Confirm Action"
|
|
message="Are you sure you want to proceed?"
|
|
onConfirm={() => {
|
|
setConfirmVisible(false);
|
|
}}
|
|
onCancel={() => setConfirmVisible(false)}
|
|
/>
|
|
|
|
<AlertModal
|
|
visible={alertVisible}
|
|
title="Critical Alert"
|
|
message="Acute Subdural Hemorrhage detected!"
|
|
iconName="alert-circle"
|
|
iconColor={Colors.error}
|
|
onDismiss={() => setAlertVisible(false)}
|
|
/>
|
|
|
|
<LoadingOverlay visible={loading} />
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: Colors.background,
|
|
},
|
|
content: {
|
|
padding: Spacing.lg,
|
|
},
|
|
search: {
|
|
marginBottom: Spacing.md,
|
|
},
|
|
infoTitle: {
|
|
fontFamily: Typography.fontFamily.bold,
|
|
fontSize: Typography.fontSize.lg,
|
|
color: Colors.textPrimary,
|
|
marginBottom: Spacing.xs,
|
|
},
|
|
infoText: {
|
|
fontFamily: Typography.fontFamily.regular,
|
|
fontSize: Typography.fontSize.md,
|
|
color: Colors.textSecondary,
|
|
marginBottom: Spacing.xs,
|
|
},
|
|
active: {
|
|
color: Colors.success,
|
|
fontFamily: Typography.fontFamily.bold,
|
|
},
|
|
caseCard: {
|
|
marginBottom: Spacing.md,
|
|
},
|
|
cardHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: Spacing.md,
|
|
},
|
|
titleRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
cardTitle: {
|
|
fontFamily: Typography.fontFamily.bold,
|
|
fontSize: Typography.fontSize.md,
|
|
color: Colors.textPrimary,
|
|
marginLeft: Spacing.sm,
|
|
},
|
|
totalCount: {
|
|
fontFamily: Typography.fontFamily.bold,
|
|
fontSize: Typography.fontSize.lg,
|
|
},
|
|
modalityContainer: {
|
|
marginBottom: Spacing.md,
|
|
},
|
|
modalityLabel: {
|
|
fontFamily: Typography.fontFamily.regular,
|
|
fontSize: Typography.fontSize.sm,
|
|
color: Colors.textSecondary,
|
|
marginBottom: Spacing.xs,
|
|
},
|
|
modalityRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-around',
|
|
},
|
|
modalityItem: {
|
|
alignItems: 'center',
|
|
flex: 1,
|
|
},
|
|
modalityType: {
|
|
fontFamily: Typography.fontFamily.regular,
|
|
fontSize: Typography.fontSize.sm,
|
|
color: Colors.textSecondary,
|
|
},
|
|
modalityCount: {
|
|
fontFamily: Typography.fontFamily.bold,
|
|
fontSize: Typography.fontSize.md,
|
|
marginTop: Spacing.xs,
|
|
},
|
|
button: {
|
|
marginTop: Spacing.sm,
|
|
},
|
|
input: {
|
|
marginVertical: Spacing.md,
|
|
},
|
|
noDataText: {
|
|
fontFamily: Typography.fontFamily.regular,
|
|
fontSize: Typography.fontSize.md,
|
|
color: Colors.textSecondary,
|
|
textAlign: 'center',
|
|
padding: Spacing.lg,
|
|
},
|
|
modalTitle: {
|
|
fontFamily: Typography.fontFamily.bold,
|
|
fontSize: Typography.fontSize.lg,
|
|
color: Colors.primary,
|
|
marginBottom: Spacing.md,
|
|
textAlign: 'center',
|
|
},
|
|
modalContent: {
|
|
marginBottom: Spacing.md,
|
|
},
|
|
modalText: {
|
|
fontFamily: Typography.fontFamily.regular,
|
|
fontSize: Typography.fontSize.md,
|
|
color: Colors.textPrimary,
|
|
marginBottom: Spacing.xs,
|
|
},
|
|
});
|
|
|
|
export default DashboardScreen;
|
|
|
|
/*
|
|
* End of File: DashboardScreen.tsx
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/
|