new onboard flow for upload id document added
This commit is contained in:
parent
7a9e610b7f
commit
848bd3ea93
@ -1,6 +1,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">NeoScan_Physician</string>
|
<string name="app_name">NeoScanPhysician</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
BIN
app/assets/images/coming-soon.jpg
Normal file
BIN
app/assets/images/coming-soon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 816 KiB |
@ -324,7 +324,7 @@ const DocumentUploadStep: React.FC<DocumentUploadStepProps> = ({
|
|||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Text style={styles.sectionTitle}>Upload your ID document</Text>
|
<Text style={styles.sectionTitle}>Upload your ID document</Text>
|
||||||
<Text style={styles.description}>
|
<Text style={styles.description}>
|
||||||
Please upload a clear photo of your government-issued ID for verification.
|
Please upload a clear photo of your hospital-issued ID for verification.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Document Upload Area */}
|
{/* Document Upload Area */}
|
||||||
|
|||||||
@ -313,7 +313,7 @@ const styles = StyleSheet.create({
|
|||||||
|
|
||||||
// Eye icon for password visibility
|
// Eye icon for password visibility
|
||||||
eyeIcon: {
|
eyeIcon: {
|
||||||
paddingLeft: theme.spacing.sm,
|
padding: theme.spacing.sm,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Base button styling
|
// Base button styling
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* File: ResetPasswordScreen.tsx
|
* File: ResetPasswordScreen.tsx
|
||||||
* Description: Password reset screen for onboarding flow
|
* Description: Password reset screen for onboarding flow with document upload support
|
||||||
* Design & Developed by Tech4Biz Solutions
|
* Design & Developed by Tech4Biz Solutions
|
||||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||||
*/
|
*/
|
||||||
@ -16,7 +16,15 @@ import {
|
|||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Platform,
|
Platform,
|
||||||
Alert,
|
Alert,
|
||||||
|
Image,
|
||||||
|
PermissionsAndroid,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import {
|
||||||
|
launchImageLibrary,
|
||||||
|
launchCamera,
|
||||||
|
ImagePickerResponse,
|
||||||
|
MediaType,
|
||||||
|
} from 'react-native-image-picker';
|
||||||
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
||||||
import { updateOnboarded, logout } from '../redux/authSlice';
|
import { updateOnboarded, logout } from '../redux/authSlice';
|
||||||
import { authAPI } from '../services/authAPI';
|
import { authAPI } from '../services/authAPI';
|
||||||
@ -49,18 +57,41 @@ interface PasswordRule {
|
|||||||
isValid: boolean;
|
isValid: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ImageData Interface
|
||||||
|
*
|
||||||
|
* Purpose: Defines the structure for image data
|
||||||
|
*/
|
||||||
|
interface ImageData {
|
||||||
|
uri: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnboardingStep Type
|
||||||
|
*
|
||||||
|
* Purpose: Defines the different onboarding steps
|
||||||
|
*/
|
||||||
|
type OnboardingStep = 'document' | 'password';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ResetPasswordScreen Component
|
* ResetPasswordScreen Component
|
||||||
*
|
*
|
||||||
* Purpose: Password reset screen for users who need to set their initial password
|
* Purpose: Password reset screen for users who need to set their initial password
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* 1. Password and confirm password input fields
|
* 1. Two flows based on platform:
|
||||||
* 2. Real-time password validation with visual feedback
|
* - platform = 'web': Document upload first, then password reset
|
||||||
* 3. Password visibility toggles
|
* - platform = 'app': Password reset only
|
||||||
* 4. Password strength requirements display
|
* 2. Document upload with camera/gallery selection
|
||||||
* 5. Integration with Redux for onboarding status
|
* 3. Password and confirm password input fields
|
||||||
* 6. API integration for password change
|
* 4. Real-time password validation with visual feedback
|
||||||
|
* 5. Password visibility toggles
|
||||||
|
* 6. Password strength requirements display
|
||||||
|
* 7. Integration with Redux for onboarding status
|
||||||
|
* 8. API integration for document upload and password change
|
||||||
*
|
*
|
||||||
* Password Requirements:
|
* Password Requirements:
|
||||||
* - At least 8 characters
|
* - At least 8 characters
|
||||||
@ -84,6 +115,13 @@ export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
|
|||||||
const [isConfirmPasswordVisible, setConfirmPasswordVisible] = useState(false);
|
const [isConfirmPasswordVisible, setConfirmPasswordVisible] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Document upload states
|
||||||
|
const [selectedImage, setSelectedImage] = useState<ImageData | null>(null);
|
||||||
|
const [documentUploaded, setDocumentUploaded] = useState(false);
|
||||||
|
|
||||||
|
// Current step state
|
||||||
|
const [currentStep, setCurrentStep] = useState<OnboardingStep>('document');
|
||||||
|
|
||||||
// Redux state
|
// Redux state
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const user = useAppSelector((state) => state.auth.user);
|
const user = useAppSelector((state) => state.auth.user);
|
||||||
@ -141,6 +179,19 @@ export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
|
|||||||
updatePasswordRules(password);
|
updatePasswordRules(password);
|
||||||
}, [password, confirmPassword]);
|
}, [password, confirmPassword]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useEffect for determining initial step
|
||||||
|
*
|
||||||
|
* Purpose: Set the initial step based on user signup platform
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (user?.platform === 'app') {
|
||||||
|
setCurrentStep('password');
|
||||||
|
} else {
|
||||||
|
setCurrentStep('document');
|
||||||
|
}
|
||||||
|
}, [user?.platform]);
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// HELPER FUNCTIONS
|
// HELPER FUNCTIONS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -181,6 +232,237 @@ export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request Camera Permission
|
||||||
|
*
|
||||||
|
* Purpose: Request camera permission for Android devices
|
||||||
|
*
|
||||||
|
* @returns Promise<boolean> - Whether permission was granted
|
||||||
|
*/
|
||||||
|
const requestCameraPermission = async (): Promise<boolean> => {
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
try {
|
||||||
|
const granted = await PermissionsAndroid.request(
|
||||||
|
PermissionsAndroid.PERMISSIONS.CAMERA,
|
||||||
|
{
|
||||||
|
title: 'Camera Permission',
|
||||||
|
message: 'This app needs camera permission to capture images.',
|
||||||
|
buttonNeutral: 'Ask Me Later',
|
||||||
|
buttonNegative: 'Cancel',
|
||||||
|
buttonPositive: 'OK',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Camera permission error:', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format File Size
|
||||||
|
*
|
||||||
|
* Purpose: Convert bytes to human readable format
|
||||||
|
*
|
||||||
|
* @param bytes - File size in bytes
|
||||||
|
* @returns Formatted file size string
|
||||||
|
*/
|
||||||
|
const formatFileSize = (bytes?: number): string => {
|
||||||
|
if (!bytes) return '';
|
||||||
|
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get File Type Display
|
||||||
|
*
|
||||||
|
* Purpose: Get display name for file type
|
||||||
|
*
|
||||||
|
* @param type - MIME type
|
||||||
|
* @returns Display name for file type
|
||||||
|
*/
|
||||||
|
const getFileTypeDisplay = (type: string): string => {
|
||||||
|
if (type.includes('jpeg') || type.includes('jpg')) return 'JPEG';
|
||||||
|
if (type.includes('png')) return 'PNG';
|
||||||
|
if (type.includes('gif')) return 'GIF';
|
||||||
|
if (type.includes('webp')) return 'WebP';
|
||||||
|
return 'Image';
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DOCUMENT UPLOAD HANDLERS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Image Picker Selection
|
||||||
|
*
|
||||||
|
* Purpose: Show options for camera or gallery selection
|
||||||
|
*/
|
||||||
|
const handleImagePicker = () => {
|
||||||
|
Alert.alert(
|
||||||
|
'Select Image',
|
||||||
|
'Choose how you want to upload your document',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Camera',
|
||||||
|
onPress: () => handleCameraCapture(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Gallery',
|
||||||
|
onPress: () => handleGalleryPicker(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Cancel',
|
||||||
|
style: 'cancel',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Camera Capture
|
||||||
|
*
|
||||||
|
* Purpose: Launch camera to capture image
|
||||||
|
*/
|
||||||
|
const handleCameraCapture = async () => {
|
||||||
|
const hasPermission = await requestCameraPermission();
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
showError('Permission Error', 'Camera permission is required to capture images.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
mediaType: 'photo' as MediaType,
|
||||||
|
maxWidth: 2000,
|
||||||
|
maxHeight: 2000,
|
||||||
|
includeBase64: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
launchCamera(options, (response: ImagePickerResponse) => {
|
||||||
|
if (response.didCancel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.errorMessage) {
|
||||||
|
showError('Camera Error', response.errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.assets && response.assets[0]) {
|
||||||
|
const asset = response.assets[0];
|
||||||
|
const imageData: ImageData = {
|
||||||
|
uri: asset.uri!,
|
||||||
|
name: asset.fileName || `document_${Date.now()}.jpg`,
|
||||||
|
type: asset.type || 'image/jpeg',
|
||||||
|
size: asset.fileSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectedImage(imageData);
|
||||||
|
showSuccess('Success', 'Document captured successfully!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Gallery Picker
|
||||||
|
*
|
||||||
|
* Purpose: Launch image library to select image
|
||||||
|
*/
|
||||||
|
const handleGalleryPicker = () => {
|
||||||
|
const options = {
|
||||||
|
mediaType: 'photo' as MediaType,
|
||||||
|
maxWidth: 2000,
|
||||||
|
maxHeight: 2000,
|
||||||
|
includeBase64: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
launchImageLibrary(options, (response: ImagePickerResponse) => {
|
||||||
|
if (response.didCancel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.errorMessage) {
|
||||||
|
showError('Gallery Error', response.errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.assets && response.assets[0]) {
|
||||||
|
const asset = response.assets[0];
|
||||||
|
const imageData: ImageData = {
|
||||||
|
uri: asset.uri!,
|
||||||
|
name: asset.fileName || `document_${Date.now()}.jpg`,
|
||||||
|
type: asset.type || 'image/jpeg',
|
||||||
|
size: asset.fileSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectedImage(imageData);
|
||||||
|
showSuccess('Success', 'Document selected from gallery!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Remove Image
|
||||||
|
*
|
||||||
|
* Purpose: Remove selected image
|
||||||
|
*/
|
||||||
|
const handleRemoveImage = () => {
|
||||||
|
setSelectedImage(null);
|
||||||
|
setDocumentUploaded(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Document Upload
|
||||||
|
*
|
||||||
|
* Purpose: Upload document to server
|
||||||
|
*/
|
||||||
|
const handleDocumentUpload = async () => {
|
||||||
|
if (!selectedImage) {
|
||||||
|
showError('Validation Error', 'Please upload a document to continue.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
const file = {
|
||||||
|
uri: selectedImage.uri,
|
||||||
|
name: selectedImage.name,
|
||||||
|
type: selectedImage.type,
|
||||||
|
};
|
||||||
|
formData.append('id_photo', file as any);
|
||||||
|
|
||||||
|
const response: any = await authAPI.uploadDocument(formData, user?.access_token);
|
||||||
|
console.log('upload response',response)
|
||||||
|
|
||||||
|
if (response.data && response.data.message) {
|
||||||
|
if (response.data.success) {
|
||||||
|
showSuccess(response.data.message);
|
||||||
|
setDocumentUploaded(true);
|
||||||
|
// Move to password step after successful upload
|
||||||
|
setCurrentStep('password');
|
||||||
|
} else {
|
||||||
|
showError(response.data.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showError('Error while uploading document');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Document upload error:', error);
|
||||||
|
showError('Failed to upload document. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// EVENT HANDLERS
|
// EVENT HANDLERS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -319,6 +601,191 @@ export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* renderDocumentUploadStep Function
|
||||||
|
*
|
||||||
|
* Purpose: Render document upload step
|
||||||
|
*
|
||||||
|
* @returns JSX element for document upload step
|
||||||
|
*/
|
||||||
|
const renderDocumentUploadStep = () => (
|
||||||
|
<>
|
||||||
|
{/* Icon */}
|
||||||
|
<View style={styles.iconContainer}>
|
||||||
|
<Icon name="file-text" size={64} color={theme.colors.primary} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Title and Subtitle */}
|
||||||
|
<Text style={styles.title}>Upload Your ID Document</Text>
|
||||||
|
<Text style={styles.subtitle}>
|
||||||
|
Please upload a clear photo of your hospital-issued ID for verification
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Document Upload Area */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.uploadContainer}
|
||||||
|
onPress={handleImagePicker}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<View style={styles.uploadContent}>
|
||||||
|
{selectedImage ? (
|
||||||
|
<View style={styles.imagePreviewContainer}>
|
||||||
|
<Image source={{ uri: selectedImage.uri }} style={styles.imagePreview} />
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.imageOverlay}
|
||||||
|
onPress={handleRemoveImage}
|
||||||
|
>
|
||||||
|
<Icon name="x" size={20} color={theme.colors.background} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.uploadedText}>Document Selected</Text>
|
||||||
|
<Text style={styles.fileName}>{selectedImage.name}</Text>
|
||||||
|
<View style={styles.fileDetails}>
|
||||||
|
<Text style={styles.fileType}>{getFileTypeDisplay(selectedImage.type)}</Text>
|
||||||
|
{selectedImage.size && (
|
||||||
|
<Text style={styles.fileSize}> • {formatFileSize(selectedImage.size)}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon name="image" size={48} color={theme.colors.textSecondary} />
|
||||||
|
<Text style={styles.uploadText}>Tap to upload document</Text>
|
||||||
|
<Text style={styles.uploadSubtext}>JPG, PNG supported</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{selectedImage && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.changeButton}
|
||||||
|
onPress={handleImagePicker}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<Text style={styles.changeButtonText}>Change Document</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Upload Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.button,
|
||||||
|
styles.uploadButton,
|
||||||
|
(!selectedImage || loading) && styles.buttonDisabled,
|
||||||
|
]}
|
||||||
|
onPress={handleDocumentUpload}
|
||||||
|
disabled={!selectedImage || loading}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<Text style={styles.buttonText}>Uploading Document...</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.buttonText}>Upload Document</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* renderPasswordResetStep Function
|
||||||
|
*
|
||||||
|
* Purpose: Render password reset step
|
||||||
|
*
|
||||||
|
* @returns JSX element for password reset step
|
||||||
|
*/
|
||||||
|
const renderPasswordResetStep = () => (
|
||||||
|
<>
|
||||||
|
{/* Icon */}
|
||||||
|
<View style={styles.iconContainer}>
|
||||||
|
<Icon name="lock" size={64} color={theme.colors.primary} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Title and Subtitle */}
|
||||||
|
<Text style={styles.title}>Set Your Password</Text>
|
||||||
|
<Text style={styles.subtitle}>
|
||||||
|
Create a strong password to complete your account setup
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Password Input */}
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Icon name="lock" size={20} color={theme.colors.textSecondary} style={styles.inputIcon} />
|
||||||
|
<TextInput
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChangeText={handlePasswordChange}
|
||||||
|
style={styles.inputField}
|
||||||
|
secureTextEntry={!isPasswordVisible}
|
||||||
|
placeholderTextColor={theme.colors.textMuted}
|
||||||
|
editable={!loading}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => setPasswordVisible(!isPasswordVisible)}
|
||||||
|
style={styles.eyeIcon}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={isPasswordVisible ? 'eye-off' : 'eye'}
|
||||||
|
size={22}
|
||||||
|
color={theme.colors.textSecondary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Confirm Password Input */}
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Icon name="lock" size={20} color={theme.colors.textSecondary} style={styles.inputIcon} />
|
||||||
|
<TextInput
|
||||||
|
placeholder="Confirm Password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChangeText={handleConfirmPasswordChange}
|
||||||
|
style={styles.inputField}
|
||||||
|
secureTextEntry={!isConfirmPasswordVisible}
|
||||||
|
placeholderTextColor={theme.colors.textMuted}
|
||||||
|
editable={!loading}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => setConfirmPasswordVisible(!isConfirmPasswordVisible)}
|
||||||
|
style={styles.eyeIcon}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={isConfirmPasswordVisible ? 'eye-off' : 'eye'}
|
||||||
|
size={22}
|
||||||
|
color={theme.colors.textSecondary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Password Rules Section */}
|
||||||
|
<View style={styles.rulesContainer}>
|
||||||
|
<Text style={styles.rulesTitle}>Password Requirements:</Text>
|
||||||
|
<View style={styles.rulesGrid}>
|
||||||
|
{passwordRules.map(renderPasswordRule)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Reset Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.button,
|
||||||
|
styles.resetButton,
|
||||||
|
loading && styles.buttonDisabled,
|
||||||
|
]}
|
||||||
|
onPress={handleReset}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<Text style={styles.buttonText}>Setting Password...</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.buttonText}>Set Password</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// RENDER SECTION
|
// RENDER SECTION
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -338,97 +805,33 @@ export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
|
|||||||
<TouchableOpacity onPress={handleBack} style={styles.backButton}>
|
<TouchableOpacity onPress={handleBack} style={styles.backButton}>
|
||||||
<Icon name="log-out" size={24} color={theme.colors.textPrimary} />
|
<Icon name="log-out" size={24} color={theme.colors.textPrimary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.headerTitle}>Set Your Password</Text>
|
<Text style={styles.headerTitle}>
|
||||||
|
{currentStep === 'document' ? 'Upload Document' : 'Set Password'}
|
||||||
|
</Text>
|
||||||
<View style={styles.headerSpacer} />
|
<View style={styles.headerSpacer} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Icon */}
|
{/* Step Indicator */}
|
||||||
<View style={styles.iconContainer}>
|
{user?.platform === 'web' && (
|
||||||
<Icon name="lock" size={64} color={theme.colors.primary} />
|
<View style={styles.stepIndicator}>
|
||||||
</View>
|
<View style={styles.stepContainer}>
|
||||||
|
<View style={[styles.stepCircle, currentStep === 'document' && styles.stepCircleActive]}>
|
||||||
{/* Title and Subtitle */}
|
<Text style={[styles.stepNumber, currentStep === 'document' && styles.stepNumberActive]}>1</Text>
|
||||||
<Text style={styles.title}>Set your password</Text>
|
</View>
|
||||||
<Text style={styles.subtitle}>
|
<Text style={[styles.stepLabel, currentStep === 'document' && styles.stepLabelActive]}>Document</Text>
|
||||||
Create a strong password to complete your account setup
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* Password Input */}
|
|
||||||
<View style={styles.inputContainer}>
|
|
||||||
<Icon name="lock" size={20} color={theme.colors.textSecondary} style={styles.inputIcon} />
|
|
||||||
<TextInput
|
|
||||||
placeholder="Password"
|
|
||||||
value={password}
|
|
||||||
onChangeText={handlePasswordChange}
|
|
||||||
style={styles.inputField}
|
|
||||||
secureTextEntry={!isPasswordVisible}
|
|
||||||
placeholderTextColor={theme.colors.textMuted}
|
|
||||||
editable={!loading}
|
|
||||||
/>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => setPasswordVisible(!isPasswordVisible)}
|
|
||||||
style={styles.eyeIcon}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={isPasswordVisible ? 'eye-off' : 'eye'}
|
|
||||||
size={22}
|
|
||||||
color={theme.colors.textSecondary}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Confirm Password Input */}
|
|
||||||
<View style={styles.inputContainer}>
|
|
||||||
<Icon name="lock" size={20} color={theme.colors.textSecondary} style={styles.inputIcon} />
|
|
||||||
<TextInput
|
|
||||||
placeholder="Confirm Password"
|
|
||||||
value={confirmPassword}
|
|
||||||
onChangeText={handleConfirmPasswordChange}
|
|
||||||
style={styles.inputField}
|
|
||||||
secureTextEntry={!isConfirmPasswordVisible}
|
|
||||||
placeholderTextColor={theme.colors.textMuted}
|
|
||||||
editable={!loading}
|
|
||||||
/>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => setConfirmPasswordVisible(!isConfirmPasswordVisible)}
|
|
||||||
style={styles.eyeIcon}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={isConfirmPasswordVisible ? 'eye-off' : 'eye'}
|
|
||||||
size={22}
|
|
||||||
color={theme.colors.textSecondary}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Password Rules Section */}
|
|
||||||
<View style={styles.rulesContainer}>
|
|
||||||
<Text style={styles.rulesTitle}>Password Requirements:</Text>
|
|
||||||
<View style={styles.rulesGrid}>
|
|
||||||
{passwordRules.map(renderPasswordRule)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Reset Button */}
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[
|
|
||||||
styles.button,
|
|
||||||
styles.resetButton,
|
|
||||||
loading && styles.buttonDisabled,
|
|
||||||
]}
|
|
||||||
onPress={handleReset}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<View style={styles.loadingContainer}>
|
|
||||||
<Text style={styles.buttonText}>Setting Password...</Text>
|
|
||||||
</View>
|
</View>
|
||||||
) : (
|
<View style={styles.stepLine} />
|
||||||
<Text style={styles.buttonText}>Set Password</Text>
|
<View style={styles.stepContainer}>
|
||||||
)}
|
<View style={[styles.stepCircle, currentStep === 'password' && styles.stepCircleActive]}>
|
||||||
</TouchableOpacity>
|
<Text style={[styles.stepNumber, currentStep === 'password' && styles.stepNumberActive]}>2</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={[styles.stepLabel, currentStep === 'password' && styles.stepLabelActive]}>Password</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Content based on current step */}
|
||||||
|
{currentStep === 'document' ? renderDocumentUploadStep() : renderPasswordResetStep()}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
);
|
);
|
||||||
@ -483,6 +886,72 @@ const styles = StyleSheet.create({
|
|||||||
width: 40,
|
width: 40,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Step indicator
|
||||||
|
stepIndicator: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: theme.spacing.xl,
|
||||||
|
paddingHorizontal: theme.spacing.lg,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Step container
|
||||||
|
stepContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Step circle
|
||||||
|
stepCircle: {
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 16,
|
||||||
|
backgroundColor: theme.colors.backgroundAlt,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: theme.spacing.xs,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Step circle active
|
||||||
|
stepCircleActive: {
|
||||||
|
backgroundColor: theme.colors.primary,
|
||||||
|
borderColor: theme.colors.primary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Step number
|
||||||
|
stepNumber: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyMedium,
|
||||||
|
fontFamily: theme.typography.fontFamily.medium,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Step number active
|
||||||
|
stepNumberActive: {
|
||||||
|
color: theme.colors.background,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Step label
|
||||||
|
stepLabel: {
|
||||||
|
fontSize: theme.typography.fontSize.bodySmall,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Step label active
|
||||||
|
stepLabelActive: {
|
||||||
|
color: theme.colors.primary,
|
||||||
|
fontFamily: theme.typography.fontFamily.medium,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Step line
|
||||||
|
stepLine: {
|
||||||
|
flex: 1,
|
||||||
|
height: 2,
|
||||||
|
backgroundColor: theme.colors.border,
|
||||||
|
marginHorizontal: theme.spacing.md,
|
||||||
|
},
|
||||||
|
|
||||||
// Icon container
|
// Icon container
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -507,6 +976,119 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: theme.spacing.xl,
|
marginBottom: theme.spacing.xl,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Upload container
|
||||||
|
uploadContainer: {
|
||||||
|
backgroundColor: theme.colors.backgroundAlt,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
borderStyle: 'dashed',
|
||||||
|
borderRadius: theme.borderRadius.medium,
|
||||||
|
padding: theme.spacing.xl,
|
||||||
|
marginBottom: theme.spacing.md,
|
||||||
|
minHeight: 200,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Upload content
|
||||||
|
uploadContent: {
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Image preview container
|
||||||
|
imagePreviewContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Image preview
|
||||||
|
imagePreview: {
|
||||||
|
width: '100%',
|
||||||
|
height: 150,
|
||||||
|
borderRadius: theme.borderRadius.medium,
|
||||||
|
marginBottom: theme.spacing.sm,
|
||||||
|
resizeMode: 'contain',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Image overlay (remove button)
|
||||||
|
imageOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
right: 20,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||||
|
borderRadius: 17,
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Upload text
|
||||||
|
uploadText: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyLarge,
|
||||||
|
fontFamily: theme.typography.fontFamily.medium,
|
||||||
|
color: theme.colors.textPrimary,
|
||||||
|
marginTop: theme.spacing.sm,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Upload subtext
|
||||||
|
uploadSubtext: {
|
||||||
|
fontSize: theme.typography.fontSize.bodySmall,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
marginTop: theme.spacing.xs,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Uploaded text
|
||||||
|
uploadedText: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyLarge,
|
||||||
|
fontFamily: theme.typography.fontFamily.medium,
|
||||||
|
color: theme.colors.success,
|
||||||
|
marginTop: theme.spacing.sm,
|
||||||
|
},
|
||||||
|
|
||||||
|
// File name
|
||||||
|
fileName: {
|
||||||
|
fontSize: theme.typography.fontSize.bodySmall,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
marginTop: theme.spacing.xs,
|
||||||
|
textAlign: 'center',
|
||||||
|
maxWidth: '80%',
|
||||||
|
},
|
||||||
|
|
||||||
|
// File details
|
||||||
|
fileDetails: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: theme.spacing.xs,
|
||||||
|
},
|
||||||
|
|
||||||
|
// File type
|
||||||
|
fileType: {
|
||||||
|
fontSize: theme.typography.fontSize.bodySmall,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
color: theme.colors.textMuted,
|
||||||
|
},
|
||||||
|
|
||||||
|
// File size
|
||||||
|
fileSize: {
|
||||||
|
fontSize: theme.typography.fontSize.bodySmall,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
color: theme.colors.textMuted,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Change button
|
||||||
|
changeButton: {
|
||||||
|
alignSelf: 'center',
|
||||||
|
marginBottom: theme.spacing.xl,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Change button text
|
||||||
|
changeButtonText: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyMedium,
|
||||||
|
fontFamily: theme.typography.fontFamily.medium,
|
||||||
|
color: theme.colors.primary,
|
||||||
|
},
|
||||||
|
|
||||||
// Input container
|
// Input container
|
||||||
inputContainer: {
|
inputContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@ -607,6 +1189,12 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: theme.spacing.md,
|
marginBottom: theme.spacing.md,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Upload button
|
||||||
|
uploadButton: {
|
||||||
|
backgroundColor: theme.colors.primary,
|
||||||
|
...theme.shadows.primary,
|
||||||
|
},
|
||||||
|
|
||||||
// Reset button
|
// Reset button
|
||||||
resetButton: {
|
resetButton: {
|
||||||
backgroundColor: theme.colors.primary,
|
backgroundColor: theme.colors.primary,
|
||||||
|
|||||||
@ -27,7 +27,9 @@ export const authAPI = {
|
|||||||
//change password
|
//change password
|
||||||
changepassword: (payload:{password:string,token:string | undefined}) => api.post('/api/auth/onboarding/change-password', {password:payload.password},buildHeaders({token:payload.token})),
|
changepassword: (payload:{password:string,token:string | undefined}) => api.post('/api/auth/onboarding/change-password', {password:payload.password},buildHeaders({token:payload.token})),
|
||||||
//validate username
|
//validate username
|
||||||
validateusername: (username:string|undefined) => api.post('/api/auth/auth/check-username', {username},buildHeaders())
|
validateusername: (username:string|undefined) => api.post('/api/auth/auth/check-username', {username},buildHeaders()),
|
||||||
|
//upload document for onboarding
|
||||||
|
uploadDocument: (formData:any, token:string | undefined) => api.post('/api/auth/onboarding/upload-id-photo', formData,buildHeaders({token:token, contentType: 'multipart/form-data' }))
|
||||||
// Add more endpoints as needed
|
// Add more endpoints as needed
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
616
app/modules/Dashboard/components/BrainPredictionsOverview.tsx
Normal file
616
app/modules/Dashboard/components/BrainPredictionsOverview.tsx
Normal file
@ -0,0 +1,616 @@
|
|||||||
|
/*
|
||||||
|
* File: BrainPredictionsOverview.tsx
|
||||||
|
* Description: Component to display patient overview statistics based on different AI prediction scenarios for brain conditions
|
||||||
|
* Design & Developed by Tech4Biz Solutions
|
||||||
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
Dimensions,
|
||||||
|
ScrollView,
|
||||||
|
} from 'react-native';
|
||||||
|
import { PieChart } from 'react-native-chart-kit';
|
||||||
|
import { theme } from '../../../theme/theme';
|
||||||
|
import { ERDashboard } from '../../../shared/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BrainPredictionsOverviewProps Interface
|
||||||
|
*
|
||||||
|
* Purpose: Defines the props required by the BrainPredictionsOverview component
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - dashboard: ERDashboard object containing brain prediction statistics
|
||||||
|
*/
|
||||||
|
interface BrainPredictionsOverviewProps {
|
||||||
|
dashboard: ERDashboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BrainPredictionsOverview Component
|
||||||
|
*
|
||||||
|
* Purpose: Display patient overview statistics based on different AI prediction scenarios
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* 1. Shows distribution of patients by brain condition predictions
|
||||||
|
* 2. Displays AI confidence levels for each prediction type
|
||||||
|
* 3. Shows critical vs non-critical brain conditions
|
||||||
|
* 4. Provides summary statistics for quick overview
|
||||||
|
* 5. Visualizes data using react-native-chart-kit pie chart
|
||||||
|
*
|
||||||
|
* Prediction Categories:
|
||||||
|
* - Hemorrhagic Conditions (IPH, IVH, SAH, SDH, EDH)
|
||||||
|
* - Ischemic Conditions (Stroke)
|
||||||
|
* - Structural Changes (Mass effect, Midline shift)
|
||||||
|
* - Normal Findings
|
||||||
|
*/
|
||||||
|
export const BrainPredictionsOverview: React.FC<BrainPredictionsOverviewProps> = ({
|
||||||
|
dashboard,
|
||||||
|
}) => {
|
||||||
|
// ============================================================================
|
||||||
|
// MOCK PREDICTION DATA
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock prediction statistics based on brain conditions
|
||||||
|
* In a real app, this would come from the AI analysis API
|
||||||
|
*/
|
||||||
|
const predictionStats = {
|
||||||
|
// Hemorrhagic Conditions
|
||||||
|
intraparenchymal: { count: 2, confidence: 94, critical: true },
|
||||||
|
intraventricular: { count: 1, confidence: 87, critical: true },
|
||||||
|
subarachnoid: { count: 1, confidence: 87, critical: true },
|
||||||
|
subdural: { count: 1, confidence: 89, critical: true },
|
||||||
|
epidural: { count: 1, confidence: 96, critical: true },
|
||||||
|
|
||||||
|
// Ischemic Conditions
|
||||||
|
ischemic_stroke: { count: 1, confidence: 92, critical: true },
|
||||||
|
|
||||||
|
// Structural Changes
|
||||||
|
mass_effect: { count: 2, confidence: 91, critical: true },
|
||||||
|
midline_shift: { count: 1, confidence: 96, critical: true },
|
||||||
|
|
||||||
|
// Normal Findings
|
||||||
|
normal_brain: { count: 3, confidence: 98, critical: false },
|
||||||
|
|
||||||
|
// Pending Analysis
|
||||||
|
pending_analysis: { count: 6, confidence: 0, critical: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// HELPER FUNCTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getPredictionLabel Function
|
||||||
|
*
|
||||||
|
* Purpose: Get human-readable label for prediction type
|
||||||
|
*
|
||||||
|
* @param predictionType - Type of brain prediction
|
||||||
|
* @returns Human-readable label
|
||||||
|
*/
|
||||||
|
const getPredictionLabel = (predictionType: string) => {
|
||||||
|
if (!predictionType) {
|
||||||
|
return 'Unknown'; // Default label for undefined types
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels: { [key: string]: string } = {
|
||||||
|
intraparenchymal: 'IPH',
|
||||||
|
intraventricular: 'IVH',
|
||||||
|
subarachnoid: 'SAH',
|
||||||
|
subdural: 'SDH',
|
||||||
|
epidural: 'EDH',
|
||||||
|
ischemic_stroke: 'Stroke',
|
||||||
|
mass_effect: 'Mass Effect',
|
||||||
|
midline_shift: 'Midline Shift',
|
||||||
|
normal_brain: 'Normal',
|
||||||
|
pending_analysis: 'Pending',
|
||||||
|
};
|
||||||
|
|
||||||
|
return labels[predictionType] || predictionType;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getPredictionColor Function
|
||||||
|
*
|
||||||
|
* Purpose: Get color based on prediction type and criticality
|
||||||
|
*
|
||||||
|
* @param predictionType - Type of brain prediction
|
||||||
|
* @param isCritical - Whether the condition is critical
|
||||||
|
* @returns Color string for styling
|
||||||
|
*/
|
||||||
|
const getPredictionColor = (predictionType: string, isCritical: boolean) => {
|
||||||
|
if (!predictionType) {
|
||||||
|
return theme.colors.textSecondary; // Default color for undefined types
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define distinct colors for each prediction class
|
||||||
|
const predictionColors: { [key: string]: string } = {
|
||||||
|
intraparenchymal: '#E53E3E', // Red for IPH
|
||||||
|
intraventricular: '#C53030', // Dark Red for IVH
|
||||||
|
subarachnoid: '#F56565', // Light Red for SAH
|
||||||
|
subdural: '#FC8181', // Pink Red for SDH
|
||||||
|
epidural: '#E53E3E', // Red for EDH
|
||||||
|
ischemic_stroke: '#3182CE', // Blue for Stroke
|
||||||
|
mass_effect: '#805AD5', // Purple for Mass Effect
|
||||||
|
midline_shift: '#553C9A', // Dark Purple for Midline Shift
|
||||||
|
normal_brain: '#38A169', // Green for Normal
|
||||||
|
pending_analysis: '#D69E2E', // Yellow for Pending
|
||||||
|
};
|
||||||
|
|
||||||
|
return predictionColors[predictionType] || theme.colors.textSecondary;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// COMPUTED VALUES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate total patients
|
||||||
|
*/
|
||||||
|
const totalPatients = Object.values(predictionStats).reduce((sum, stats) => sum + stats.count, 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate total critical conditions
|
||||||
|
*/
|
||||||
|
const totalCritical = Object.values(predictionStats)
|
||||||
|
.filter(stats => stats.critical)
|
||||||
|
.reduce((sum, stats) => sum + stats.count, 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate total normal findings
|
||||||
|
*/
|
||||||
|
const totalNormal = predictionStats.normal_brain.count;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate total pending analysis
|
||||||
|
*/
|
||||||
|
const totalPending = predictionStats.pending_analysis.count;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate average confidence for critical conditions
|
||||||
|
*/
|
||||||
|
const criticalConditions = Object.values(predictionStats).filter(stats => stats.critical && stats.confidence > 0);
|
||||||
|
const averageConfidence = criticalConditions.length > 0
|
||||||
|
? Math.round(criticalConditions.reduce((sum, stats) => sum + stats.confidence, 0) / criticalConditions.length)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all prediction classes by count
|
||||||
|
*/
|
||||||
|
const allPredictions = Object.entries(predictionStats)
|
||||||
|
.filter(([_, stats]) => stats.count > 0)
|
||||||
|
.sort((a, b) => b[1].count - a[1].count);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CHART DATA PREPARATION
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare chart data for react-native-chart-kit PieChart
|
||||||
|
*/
|
||||||
|
const chartData = Object.entries(predictionStats)
|
||||||
|
.filter(([_, stats]) => stats && stats.count > 0) // Filter out undefined or zero count entries
|
||||||
|
.map(([predictionType, stats]) => ({
|
||||||
|
name: getPredictionLabel(predictionType),
|
||||||
|
population: stats.count, // react-native-chart-kit uses 'population' property
|
||||||
|
color: getPredictionColor(predictionType, stats.critical),
|
||||||
|
legendFontColor: theme.colors.textPrimary,
|
||||||
|
legendFontSize: 12,
|
||||||
|
confidence: stats.confidence,
|
||||||
|
isCritical: stats.critical,
|
||||||
|
}))
|
||||||
|
.filter(item => item.population > 0) // Additional filter to ensure no zero count items
|
||||||
|
.sort((a, b) => b.population - a.population); // Sort by count descending - removed limit to show all classes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chart configuration for react-native-chart-kit
|
||||||
|
*/
|
||||||
|
const chartConfig = {
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
backgroundGradientFrom: theme.colors.background,
|
||||||
|
backgroundGradientTo: theme.colors.background,
|
||||||
|
decimalPlaces: 0,
|
||||||
|
color: (opacity = 1) => `rgba(33, 150, 243, ${opacity})`,
|
||||||
|
labelColor: (opacity = 1) => `rgba(33, 33, 33, ${opacity})`,
|
||||||
|
style: {
|
||||||
|
borderRadius: 16,
|
||||||
|
},
|
||||||
|
propsForDots: {
|
||||||
|
r: '6',
|
||||||
|
strokeWidth: '2',
|
||||||
|
stroke: theme.colors.primary,
|
||||||
|
},
|
||||||
|
useShadowColorFromDataset: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// RENDER FUNCTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* renderLegend Function
|
||||||
|
*
|
||||||
|
* Purpose: Render chart legend with confidence levels in grid layout
|
||||||
|
*
|
||||||
|
* @returns Legend component
|
||||||
|
*/
|
||||||
|
const renderLegend = () => (
|
||||||
|
<View style={styles.legendContainer}>
|
||||||
|
<Text style={styles.legendTitle}>Prediction Classes Breakdown</Text>
|
||||||
|
<View style={styles.legendGrid}>
|
||||||
|
{chartData.map((item, index) => (
|
||||||
|
<View key={index} style={styles.legendGridItem}>
|
||||||
|
<View style={styles.legendItemHeader}>
|
||||||
|
<View style={[styles.legendColor, { backgroundColor: item.color }]} />
|
||||||
|
<Text style={styles.legendLabel}>{item.name}</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.legendCount}>{item.population} patients</Text>
|
||||||
|
{item.confidence > 0 && (
|
||||||
|
<Text style={styles.legendConfidence}>{item.confidence}%</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MAIN RENDER
|
||||||
|
// ============================================================================
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Section Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.title}>Brain AI Predictions Overview</Text>
|
||||||
|
<Text style={styles.subtitle}>
|
||||||
|
Patient distribution by AI-detected conditions
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Pie Chart Section */}
|
||||||
|
<View style={styles.chartSection}>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
style={styles.chartScrollView}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{chartData.length > 0 ? (
|
||||||
|
<View style={styles.chartContent}>
|
||||||
|
<View style={styles.pieChartContainer}>
|
||||||
|
{(() => {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
<PieChart
|
||||||
|
data={chartData}
|
||||||
|
width={Dimensions.get('window').width}
|
||||||
|
height={250}
|
||||||
|
accessor="population"
|
||||||
|
backgroundColor="transparent"
|
||||||
|
paddingLeft="0"
|
||||||
|
absolute
|
||||||
|
hasLegend={false}
|
||||||
|
chartConfig={chartConfig}
|
||||||
|
center={[Dimensions.get('window').width/4, 0]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('PieChart error:', error);
|
||||||
|
return (
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<Text style={styles.errorText}>Chart temporarily unavailable</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
</View>
|
||||||
|
{renderLegend()}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={styles.noDataContainer}>
|
||||||
|
<Text style={styles.noDataText}>No data available for chart</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Summary Statistics */}
|
||||||
|
<View style={styles.summaryContainer}>
|
||||||
|
<View style={styles.summaryItem}>
|
||||||
|
<Text style={styles.summaryLabel}>Total Patients</Text>
|
||||||
|
<Text style={styles.summaryValue}>{totalPatients}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.summaryItem}>
|
||||||
|
<Text style={styles.summaryLabel}>Critical Cases</Text>
|
||||||
|
<Text style={[styles.summaryValue, { color: theme.colors.error }]}>
|
||||||
|
{totalCritical}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.summaryItem}>
|
||||||
|
<Text style={styles.summaryLabel}>Pending Analysis</Text>
|
||||||
|
<Text style={[styles.summaryValue, { color: theme.colors.warning }]}>
|
||||||
|
{totalPending}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.summaryItem}>
|
||||||
|
<Text style={styles.summaryLabel}>Avg Confidence</Text>
|
||||||
|
<Text style={[styles.summaryValue, { color: theme.colors.primary }]}>
|
||||||
|
{averageConfidence}%
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STYLES SECTION
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
// Main container
|
||||||
|
container: {
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
borderRadius: theme.borderRadius.large,
|
||||||
|
padding: theme.spacing.md,
|
||||||
|
marginVertical: theme.spacing.sm,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Header section
|
||||||
|
header: {
|
||||||
|
marginBottom: theme.spacing.lg,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Main title
|
||||||
|
title: {
|
||||||
|
fontSize: theme.typography.fontSize.displaySmall,
|
||||||
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
|
color: theme.colors.textPrimary,
|
||||||
|
marginBottom: theme.spacing.xs,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Subtitle
|
||||||
|
subtitle: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyMedium,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Statistics grid
|
||||||
|
statsGrid: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: theme.spacing.xs,
|
||||||
|
marginBottom: theme.spacing.lg,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Individual stat card
|
||||||
|
statCard: {
|
||||||
|
flex: 1,
|
||||||
|
minWidth: '30%',
|
||||||
|
maxWidth: '32%',
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
borderRadius: theme.borderRadius.medium,
|
||||||
|
padding: theme.spacing.sm,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
borderLeftWidth: 3,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Stat value
|
||||||
|
statValue: {
|
||||||
|
fontSize: theme.typography.fontSize.displaySmall,
|
||||||
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
|
color: theme.colors.textPrimary,
|
||||||
|
marginBottom: theme.spacing.xs,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Stat title
|
||||||
|
statTitle: {
|
||||||
|
fontSize: theme.typography.fontSize.bodySmall,
|
||||||
|
fontFamily: theme.typography.fontFamily.semibold,
|
||||||
|
color: theme.colors.textPrimary,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: theme.spacing.xs,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Stat subtitle
|
||||||
|
statSubtitle: {
|
||||||
|
fontSize: theme.typography.fontSize.caption,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Chart section
|
||||||
|
chartSection: {
|
||||||
|
marginBottom: theme.spacing.lg,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Chart title
|
||||||
|
chartTitle: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyLarge,
|
||||||
|
fontFamily: theme.typography.fontFamily.semibold,
|
||||||
|
color: theme.colors.textPrimary,
|
||||||
|
marginBottom: theme.spacing.xs,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Chart subtitle
|
||||||
|
chartSubtitle: {
|
||||||
|
fontSize: theme.typography.fontSize.bodySmall,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
marginBottom: theme.spacing.md,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Chart scroll view
|
||||||
|
chartScrollView: {
|
||||||
|
// Removed maxHeight to allow full content to be visible
|
||||||
|
},
|
||||||
|
|
||||||
|
// Chart content
|
||||||
|
chartContent: {
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: theme.spacing.sm,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Pie chart container
|
||||||
|
pieChartContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: theme.spacing.lg,
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
borderRadius: theme.borderRadius.medium,
|
||||||
|
padding: theme.spacing.md,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Legend container
|
||||||
|
legendContainer: {
|
||||||
|
width: '100%',
|
||||||
|
paddingHorizontal: theme.spacing.sm,
|
||||||
|
backgroundColor: theme.colors.backgroundAlt,
|
||||||
|
borderRadius: theme.borderRadius.medium,
|
||||||
|
padding: theme.spacing.md,
|
||||||
|
marginTop: theme.spacing.md,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Legend title
|
||||||
|
legendTitle: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyLarge,
|
||||||
|
fontFamily: theme.typography.fontFamily.semibold,
|
||||||
|
color: theme.colors.textPrimary,
|
||||||
|
marginBottom: theme.spacing.md,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Legend grid container
|
||||||
|
legendGrid: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: theme.spacing.sm,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Legend grid item
|
||||||
|
legendGridItem: {
|
||||||
|
width: '48%',
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
borderRadius: theme.borderRadius.small,
|
||||||
|
padding: theme.spacing.sm,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
marginBottom: theme.spacing.xs,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Legend item header
|
||||||
|
legendItemHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: theme.spacing.xs,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Legend color
|
||||||
|
legendColor: {
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginRight: theme.spacing.xs,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Legend label
|
||||||
|
legendLabel: {
|
||||||
|
fontSize: theme.typography.fontSize.bodySmall,
|
||||||
|
fontFamily: theme.typography.fontFamily.semibold,
|
||||||
|
color: theme.colors.textPrimary,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Legend count
|
||||||
|
legendCount: {
|
||||||
|
fontSize: theme.typography.fontSize.caption,
|
||||||
|
fontFamily: theme.typography.fontFamily.medium,
|
||||||
|
color: theme.colors.textPrimary,
|
||||||
|
marginBottom: theme.spacing.xs,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Legend confidence
|
||||||
|
legendConfidence: {
|
||||||
|
fontSize: theme.typography.fontSize.caption,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// No data container
|
||||||
|
noDataContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: theme.spacing.md,
|
||||||
|
},
|
||||||
|
|
||||||
|
// No data text
|
||||||
|
noDataText: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyMedium,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Summary container
|
||||||
|
summaryContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-around',
|
||||||
|
paddingTop: theme.spacing.md,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: theme.colors.border,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Summary item
|
||||||
|
summaryItem: {
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Summary label
|
||||||
|
summaryLabel: {
|
||||||
|
fontSize: theme.typography.fontSize.bodySmall,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
marginBottom: theme.spacing.xs,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Summary value
|
||||||
|
summaryValue: {
|
||||||
|
fontSize: theme.typography.fontSize.displaySmall,
|
||||||
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
|
color: theme.colors.textPrimary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Error container
|
||||||
|
errorContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: theme.spacing.md,
|
||||||
|
backgroundColor: theme.colors.backgroundAlt,
|
||||||
|
borderRadius: theme.borderRadius.medium,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Error text
|
||||||
|
errorText: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyMedium,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* End of File: BrainPredictionsOverview.tsx
|
||||||
|
* Design & Developed by Tech4Biz Solutions
|
||||||
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||||
|
*/
|
||||||
@ -119,8 +119,9 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: theme.borderRadius.medium,
|
borderRadius: theme.borderRadius.medium,
|
||||||
padding: theme.spacing.md,
|
padding: theme.spacing.md,
|
||||||
marginRight: theme.spacing.md,
|
marginRight: theme.spacing.md,
|
||||||
minWidth: 280,
|
// minWidth: 280,
|
||||||
...theme.shadows.small,
|
...theme.shadows.small,
|
||||||
|
width:280
|
||||||
},
|
},
|
||||||
alertHeader: {
|
alertHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
6
app/modules/Dashboard/components/index.ts
Normal file
6
app/modules/Dashboard/components/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export { PatientCard } from './PatientCard';
|
||||||
|
export { CriticalAlerts } from './CriticalAlerts';
|
||||||
|
export { DashboardHeader } from './DashboardHeader';
|
||||||
|
export { QuickActions } from './QuickActions';
|
||||||
|
export { DepartmentStats } from './DepartmentStats';
|
||||||
|
export { BrainPredictionsOverview } from './BrainPredictionsOverview';
|
||||||
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* File: ERDashboardScreen.tsx
|
* File: ERDashboardScreen.tsx
|
||||||
* Description: Main ER dashboard screen displaying patient list, critical alerts, and department statistics
|
* Description: Brain AI Analysis Dashboard - Main dashboard for neurological AI predictions and brain imaging analysis
|
||||||
* Design & Developed by Tech4Biz Solutions
|
* Design & Developed by Tech4Biz Solutions
|
||||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||||
*/
|
*/
|
||||||
@ -22,7 +22,7 @@ import { PatientCard } from '../components/PatientCard';
|
|||||||
import { CriticalAlerts } from '../components/CriticalAlerts';
|
import { CriticalAlerts } from '../components/CriticalAlerts';
|
||||||
import { DashboardHeader } from '../components/DashboardHeader';
|
import { DashboardHeader } from '../components/DashboardHeader';
|
||||||
import { QuickActions } from '../components/QuickActions';
|
import { QuickActions } from '../components/QuickActions';
|
||||||
import { DepartmentStats } from '../components/DepartmentStats';
|
import { BrainPredictionsOverview } from '../components/BrainPredictionsOverview';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ERDashboardScreenProps Interface
|
* ERDashboardScreenProps Interface
|
||||||
@ -36,30 +36,52 @@ interface ERDashboardScreenProps {
|
|||||||
navigation: any;
|
navigation: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Brain AI Prediction Types
|
||||||
|
*
|
||||||
|
* Purpose: Defines the different types of brain conditions that AI can predict
|
||||||
|
*/
|
||||||
|
type BrainCondition =
|
||||||
|
| 'Intraparenchymal hemorrhage (IPH)'
|
||||||
|
| 'Intraventricular hemorrhage (IVH)'
|
||||||
|
| 'Subarachnoid hemorrhage (SAH)'
|
||||||
|
| 'Subdural hematoma (SDH)'
|
||||||
|
| 'Epidural hematoma (EDH)'
|
||||||
|
| 'Mass effect'
|
||||||
|
| 'Midline shift'
|
||||||
|
| 'Intracranial hemorrhage (ICH)'
|
||||||
|
| 'Stroke (ischemic)'
|
||||||
|
| 'Normal brain';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ERDashboardScreen Component
|
* ERDashboardScreen Component
|
||||||
*
|
*
|
||||||
* Purpose: Main dashboard screen for Emergency Department physicians
|
* Purpose: Brain AI Analysis Dashboard for Emergency Department physicians
|
||||||
*
|
*
|
||||||
* Dashboard Features:
|
* Dashboard Features:
|
||||||
* 1. Real-time patient overview with filtering capabilities
|
* 1. Real-time brain scan analysis with AI predictions
|
||||||
* 2. Critical alerts display for immediate attention
|
* 2. Critical neurological alerts for immediate attention
|
||||||
* 3. Quick action buttons for common tasks
|
* 3. Quick action buttons for brain imaging tasks
|
||||||
* 4. Department statistics and bed occupancy
|
* 4. Department statistics focused on neurological cases
|
||||||
* 5. Pull-to-refresh functionality for live updates
|
* 5. Pull-to-refresh functionality for live updates
|
||||||
* 6. Internal data generation for demonstration
|
* 6. AI prediction confidence scores and analysis
|
||||||
*
|
*
|
||||||
* Patient Filtering:
|
* Brain Condition Filtering:
|
||||||
* - All: Shows all patients in the system
|
* - All: Shows all brain scan cases
|
||||||
* - Critical: Shows only patients with critical priority
|
* - Critical: Shows only cases with critical brain conditions
|
||||||
* - Active: Shows only patients with active status
|
* - Pending: Shows cases awaiting AI analysis
|
||||||
*
|
*
|
||||||
* Data Flow:
|
* AI Prediction Classes:
|
||||||
* 1. Generate mock data internally for demonstration
|
* - Intraparenchymal hemorrhage (IPH)
|
||||||
* 2. Filter patients based on selected filter
|
* - Intraventricular hemorrhage (IVH)
|
||||||
* 3. Display critical alerts at the top
|
* - Subarachnoid hemorrhage (SAH)
|
||||||
* 4. Show patient cards in scrollable list
|
* - Subdural hematoma (SDH)
|
||||||
* 5. Handle refresh and navigation interactions
|
* - Epidural hematoma (EDH)
|
||||||
|
* - Mass effect
|
||||||
|
* - Midline shift
|
||||||
|
* - Intracranial hemorrhage (ICH)
|
||||||
|
* - Stroke (ischemic)
|
||||||
|
* - Normal brain
|
||||||
*/
|
*/
|
||||||
export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||||
navigation,
|
navigation,
|
||||||
@ -71,8 +93,8 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
// Refresh state for pull-to-refresh functionality
|
// Refresh state for pull-to-refresh functionality
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
// Patient filter state to control which patients are displayed
|
// Patient filter state to control which brain cases are displayed
|
||||||
const [selectedFilter, setSelectedFilter] = useState<'all' | 'critical' | 'active'>('all');
|
const [selectedFilter, setSelectedFilter] = useState<'all' | 'critical' | 'pending'>('all');
|
||||||
|
|
||||||
// Dashboard data state
|
// Dashboard data state
|
||||||
const [dashboard, setDashboard] = useState<ERDashboard | null>(null);
|
const [dashboard, setDashboard] = useState<ERDashboard | null>(null);
|
||||||
@ -87,31 +109,31 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
/**
|
/**
|
||||||
* generateMockDashboard Function
|
* generateMockDashboard Function
|
||||||
*
|
*
|
||||||
* Purpose: Generate mock dashboard data for demonstration
|
* Purpose: Generate mock dashboard data focused on brain AI analysis
|
||||||
*
|
*
|
||||||
* Returns: ERDashboard object with realistic ER statistics
|
* Returns: ERDashboard object with brain imaging statistics
|
||||||
*/
|
*/
|
||||||
const generateMockDashboard = (): ERDashboard => ({
|
const generateMockDashboard = (): ERDashboard => ({
|
||||||
totalPatients: 24, // Total number of patients in ER
|
totalPatients: 18, // Total number of brain scan cases
|
||||||
criticalPatients: 3, // Number of patients requiring immediate attention
|
criticalPatients: 5, // Number of critical brain conditions
|
||||||
pendingScans: 8, // Number of scans waiting for review
|
pendingScans: 6, // Number of scans awaiting AI analysis
|
||||||
recentReports: 12, // Number of reports generated recently
|
recentReports: 12, // Number of AI analysis reports generated
|
||||||
bedOccupancy: 85, // Percentage of beds currently occupied
|
bedOccupancy: 78, // Percentage of neurological beds occupied
|
||||||
departmentStats: {
|
departmentStats: {
|
||||||
emergency: 8, // Patients in emergency department
|
emergency: 8, // Emergency brain cases
|
||||||
trauma: 4, // Patients in trauma department
|
trauma: 4, // Traumatic brain injury cases
|
||||||
cardiac: 3, // Patients in cardiac department
|
cardiac: 3, // Stroke cases (using cardiac as placeholder)
|
||||||
neurology: 2, // Patients in neurology department
|
neurology: 2, // General neurology cases
|
||||||
pediatrics: 5, // Patients in pediatrics department
|
pediatrics: 1, // Neurosurgery cases (using pediatrics as placeholder)
|
||||||
icu: 2, // Patients in ICU
|
icu: 0, // ICU brain cases
|
||||||
},
|
},
|
||||||
shiftInfo: {
|
shiftInfo: {
|
||||||
currentShift: 'DAY' as const, // Current shift (DAY/NIGHT)
|
currentShift: 'DAY' as const, // Current shift (DAY/NIGHT)
|
||||||
startTime: new Date(), // Shift start time
|
startTime: new Date(), // Shift start time
|
||||||
endTime: new Date(), // Shift end time
|
endTime: new Date(), // Shift end time
|
||||||
attendingPhysician: 'Dr. Smith', // Lead physician on duty
|
attendingPhysician: 'Dr. Smith', // Lead neurologist on duty
|
||||||
residents: ['Dr. Johnson', 'Dr. Williams'], // Resident physicians
|
residents: ['Dr. Johnson', 'Dr. Williams'], // Neurology residents
|
||||||
nurses: ['Nurse Brown', 'Nurse Davis'], // Nursing staff
|
nurses: ['Nurse Brown', 'Nurse Davis'], // Neurology nursing staff
|
||||||
},
|
},
|
||||||
lastUpdated: new Date(), // Last time dashboard was updated
|
lastUpdated: new Date(), // Last time dashboard was updated
|
||||||
});
|
});
|
||||||
@ -119,30 +141,30 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
/**
|
/**
|
||||||
* generateMockPatients Function
|
* generateMockPatients Function
|
||||||
*
|
*
|
||||||
* Purpose: Generate mock patient data for demonstration
|
* Purpose: Generate mock patient data focused on brain conditions and AI predictions
|
||||||
*
|
*
|
||||||
* Returns: Array of Patient objects with realistic medical data
|
* Returns: Array of Patient objects with brain-related medical data
|
||||||
*/
|
*/
|
||||||
const generateMockPatients = (): Patient[] => [
|
const generateMockPatients = (): Patient[] => [
|
||||||
{
|
{
|
||||||
id: '1', // Unique patient identifier
|
id: '1',
|
||||||
mrn: 'MRN001', // Medical Record Number
|
mrn: 'MRN001',
|
||||||
firstName: 'John',
|
firstName: 'John',
|
||||||
lastName: 'Doe',
|
lastName: 'Doe',
|
||||||
dateOfBirth: new Date('1985-03-15'),
|
dateOfBirth: new Date('1985-03-15'),
|
||||||
gender: 'MALE' as const,
|
gender: 'MALE' as const,
|
||||||
age: 38,
|
age: 38,
|
||||||
bedNumber: 'A1', // Assigned bed number
|
bedNumber: 'A1',
|
||||||
roomNumber: '101', // Room number
|
roomNumber: '101',
|
||||||
admissionDate: new Date('2024-01-15'),
|
admissionDate: new Date('2024-01-15'),
|
||||||
status: 'ACTIVE' as const, // Current patient status
|
status: 'ACTIVE' as const,
|
||||||
priority: 'CRITICAL' as const, // Priority level for treatment
|
priority: 'CRITICAL' as const,
|
||||||
department: 'Emergency',
|
department: 'Emergency',
|
||||||
attendingPhysician: 'Dr. Smith',
|
attendingPhysician: 'Dr. Smith',
|
||||||
allergies: [
|
allergies: [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'Penicillin',
|
name: 'Contrast Dye',
|
||||||
severity: 'SEVERE' as const,
|
severity: 'SEVERE' as const,
|
||||||
reaction: 'Anaphylaxis'
|
reaction: 'Anaphylaxis'
|
||||||
},
|
},
|
||||||
@ -150,24 +172,24 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
medications: [
|
medications: [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'Morphine',
|
name: 'Mannitol',
|
||||||
dosage: '2mg',
|
dosage: '100ml',
|
||||||
frequency: 'Every 4 hours',
|
frequency: 'Every 6 hours',
|
||||||
route: 'IV', // Administration route
|
route: 'IV',
|
||||||
startDate: new Date(),
|
startDate: new Date(),
|
||||||
status: 'ACTIVE' as const,
|
status: 'ACTIVE' as const,
|
||||||
prescribedBy: 'Dr. Smith',
|
prescribedBy: 'Dr. Smith',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
vitalSigns: {
|
vitalSigns: {
|
||||||
bloodPressure: { systolic: 140, diastolic: 90, timestamp: new Date() },
|
bloodPressure: { systolic: 180, diastolic: 110, timestamp: new Date() },
|
||||||
heartRate: { value: 95, timestamp: new Date() },
|
heartRate: { value: 95, timestamp: new Date() },
|
||||||
temperature: { value: 37.2, timestamp: new Date() },
|
temperature: { value: 37.2, timestamp: new Date() },
|
||||||
respiratoryRate: { value: 18, timestamp: new Date() },
|
respiratoryRate: { value: 18, timestamp: new Date() },
|
||||||
oxygenSaturation: { value: 98, timestamp: new Date() },
|
oxygenSaturation: { value: 98, timestamp: new Date() },
|
||||||
},
|
},
|
||||||
medicalHistory: [],
|
medicalHistory: [],
|
||||||
currentDiagnosis: 'Chest pain, rule out MI', // Current medical diagnosis
|
currentDiagnosis: 'AI Predicted: Intraparenchymal hemorrhage (IPH) - 94% confidence',
|
||||||
lastUpdated: new Date(),
|
lastUpdated: new Date(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -182,20 +204,31 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
roomNumber: '102',
|
roomNumber: '102',
|
||||||
admissionDate: new Date('2024-01-15'),
|
admissionDate: new Date('2024-01-15'),
|
||||||
status: 'ACTIVE' as const,
|
status: 'ACTIVE' as const,
|
||||||
priority: 'HIGH' as const,
|
priority: 'CRITICAL' as const,
|
||||||
department: 'Trauma',
|
department: 'Trauma',
|
||||||
attendingPhysician: 'Dr. Johnson',
|
attendingPhysician: 'Dr. Johnson',
|
||||||
allergies: [],
|
allergies: [],
|
||||||
medications: [],
|
medications: [
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Dexamethasone',
|
||||||
|
dosage: '4mg',
|
||||||
|
frequency: 'Every 6 hours',
|
||||||
|
route: 'IV',
|
||||||
|
startDate: new Date(),
|
||||||
|
status: 'ACTIVE' as const,
|
||||||
|
prescribedBy: 'Dr. Johnson',
|
||||||
|
},
|
||||||
|
],
|
||||||
vitalSigns: {
|
vitalSigns: {
|
||||||
bloodPressure: { systolic: 120, diastolic: 80, timestamp: new Date() },
|
bloodPressure: { systolic: 160, diastolic: 90, timestamp: new Date() },
|
||||||
heartRate: { value: 88, timestamp: new Date() },
|
heartRate: { value: 88, timestamp: new Date() },
|
||||||
temperature: { value: 36.8, timestamp: new Date() },
|
temperature: { value: 36.8, timestamp: new Date() },
|
||||||
respiratoryRate: { value: 16, timestamp: new Date() },
|
respiratoryRate: { value: 16, timestamp: new Date() },
|
||||||
oxygenSaturation: { value: 99, timestamp: new Date() },
|
oxygenSaturation: { value: 99, timestamp: new Date() },
|
||||||
},
|
},
|
||||||
medicalHistory: [],
|
medicalHistory: [],
|
||||||
currentDiagnosis: 'Multiple trauma from MVA', // MVA = Motor Vehicle Accident
|
currentDiagnosis: 'AI Predicted: Subdural hematoma (SDH) with mass effect - 89% confidence',
|
||||||
lastUpdated: new Date(),
|
lastUpdated: new Date(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -210,8 +243,8 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
roomNumber: '103',
|
roomNumber: '103',
|
||||||
admissionDate: new Date('2024-01-15'),
|
admissionDate: new Date('2024-01-15'),
|
||||||
status: 'PENDING' as const,
|
status: 'PENDING' as const,
|
||||||
priority: 'MEDIUM' as const,
|
priority: 'HIGH' as const,
|
||||||
department: 'Cardiac',
|
department: 'Stroke',
|
||||||
attendingPhysician: 'Dr. Williams',
|
attendingPhysician: 'Dr. Williams',
|
||||||
allergies: [
|
allergies: [
|
||||||
{
|
{
|
||||||
@ -223,25 +256,25 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
],
|
],
|
||||||
medications: [
|
medications: [
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '3',
|
||||||
name: 'Nitroglycerin',
|
name: 'tPA',
|
||||||
dosage: '0.4mg',
|
dosage: '0.9mg/kg',
|
||||||
frequency: 'As needed',
|
frequency: 'Single dose',
|
||||||
route: 'Sublingual',
|
route: 'IV',
|
||||||
startDate: new Date(),
|
startDate: new Date(),
|
||||||
status: 'ACTIVE' as const,
|
status: 'ACTIVE' as const,
|
||||||
prescribedBy: 'Dr. Williams',
|
prescribedBy: 'Dr. Williams',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
vitalSigns: {
|
vitalSigns: {
|
||||||
bloodPressure: { systolic: 160, diastolic: 100, timestamp: new Date() },
|
bloodPressure: { systolic: 140, diastolic: 85, timestamp: new Date() },
|
||||||
heartRate: { value: 110, timestamp: new Date() },
|
heartRate: { value: 110, timestamp: new Date() },
|
||||||
temperature: { value: 36.9, timestamp: new Date() },
|
temperature: { value: 36.9, timestamp: new Date() },
|
||||||
respiratoryRate: { value: 20, timestamp: new Date() },
|
respiratoryRate: { value: 20, timestamp: new Date() },
|
||||||
oxygenSaturation: { value: 95, timestamp: new Date() },
|
oxygenSaturation: { value: 95, timestamp: new Date() },
|
||||||
},
|
},
|
||||||
medicalHistory: [],
|
medicalHistory: [],
|
||||||
currentDiagnosis: 'Hypertension, chest pain',
|
currentDiagnosis: 'AI Predicted: Stroke (ischemic) - 92% confidence',
|
||||||
lastUpdated: new Date(),
|
lastUpdated: new Date(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -257,15 +290,15 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
admissionDate: new Date('2024-01-15'),
|
admissionDate: new Date('2024-01-15'),
|
||||||
status: 'ACTIVE' as const,
|
status: 'ACTIVE' as const,
|
||||||
priority: 'CRITICAL' as const,
|
priority: 'CRITICAL' as const,
|
||||||
department: 'Neurology',
|
department: 'Neurosurgery',
|
||||||
attendingPhysician: 'Dr. Davis',
|
attendingPhysician: 'Dr. Davis',
|
||||||
allergies: [],
|
allergies: [],
|
||||||
medications: [
|
medications: [
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '4',
|
||||||
name: 'Mannitol',
|
name: 'Phenytoin',
|
||||||
dosage: '100ml',
|
dosage: '100mg',
|
||||||
frequency: 'Every 6 hours',
|
frequency: 'Every 8 hours',
|
||||||
route: 'IV',
|
route: 'IV',
|
||||||
startDate: new Date(),
|
startDate: new Date(),
|
||||||
status: 'ACTIVE' as const,
|
status: 'ACTIVE' as const,
|
||||||
@ -273,14 +306,14 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
vitalSigns: {
|
vitalSigns: {
|
||||||
bloodPressure: { systolic: 180, diastolic: 110, timestamp: new Date() },
|
bloodPressure: { systolic: 190, diastolic: 120, timestamp: new Date() },
|
||||||
heartRate: { value: 60, timestamp: new Date() },
|
heartRate: { value: 60, timestamp: new Date() },
|
||||||
temperature: { value: 38.5, timestamp: new Date() },
|
temperature: { value: 38.5, timestamp: new Date() },
|
||||||
respiratoryRate: { value: 12, timestamp: new Date() },
|
respiratoryRate: { value: 12, timestamp: new Date() },
|
||||||
oxygenSaturation: { value: 92, timestamp: new Date() },
|
oxygenSaturation: { value: 92, timestamp: new Date() },
|
||||||
},
|
},
|
||||||
medicalHistory: [],
|
medicalHistory: [],
|
||||||
currentDiagnosis: 'Suspected intracranial hemorrhage',
|
currentDiagnosis: 'AI Predicted: Epidural hematoma (EDH) with midline shift - 96% confidence',
|
||||||
lastUpdated: new Date(),
|
lastUpdated: new Date(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -296,7 +329,7 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
admissionDate: new Date('2024-01-15'),
|
admissionDate: new Date('2024-01-15'),
|
||||||
status: 'ACTIVE' as const,
|
status: 'ACTIVE' as const,
|
||||||
priority: 'HIGH' as const,
|
priority: 'HIGH' as const,
|
||||||
department: 'Pediatrics',
|
department: 'Neurology',
|
||||||
attendingPhysician: 'Dr. Brown',
|
attendingPhysician: 'Dr. Brown',
|
||||||
allergies: [
|
allergies: [
|
||||||
{
|
{
|
||||||
@ -308,14 +341,42 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
],
|
],
|
||||||
medications: [],
|
medications: [],
|
||||||
vitalSigns: {
|
vitalSigns: {
|
||||||
bloodPressure: { systolic: 110, diastolic: 70, timestamp: new Date() },
|
bloodPressure: { systolic: 130, diastolic: 80, timestamp: new Date() },
|
||||||
heartRate: { value: 85, timestamp: new Date() },
|
heartRate: { value: 85, timestamp: new Date() },
|
||||||
temperature: { value: 37.8, timestamp: new Date() },
|
temperature: { value: 37.8, timestamp: new Date() },
|
||||||
respiratoryRate: { value: 22, timestamp: new Date() },
|
respiratoryRate: { value: 22, timestamp: new Date() },
|
||||||
oxygenSaturation: { value: 97, timestamp: new Date() },
|
oxygenSaturation: { value: 97, timestamp: new Date() },
|
||||||
},
|
},
|
||||||
medicalHistory: [],
|
medicalHistory: [],
|
||||||
currentDiagnosis: 'Fever of unknown origin',
|
currentDiagnosis: 'AI Predicted: Subarachnoid hemorrhage (SAH) - 87% confidence',
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
mrn: 'MRN006',
|
||||||
|
firstName: 'Emily',
|
||||||
|
lastName: 'Johnson',
|
||||||
|
dateOfBirth: new Date('1988-12-05'),
|
||||||
|
gender: 'FEMALE' as const,
|
||||||
|
age: 35,
|
||||||
|
bedNumber: 'F6',
|
||||||
|
roomNumber: '106',
|
||||||
|
admissionDate: new Date('2024-01-15'),
|
||||||
|
status: 'PENDING' as const,
|
||||||
|
priority: 'MEDIUM' as const,
|
||||||
|
department: 'Emergency',
|
||||||
|
attendingPhysician: 'Dr. Wilson',
|
||||||
|
allergies: [],
|
||||||
|
medications: [],
|
||||||
|
vitalSigns: {
|
||||||
|
bloodPressure: { systolic: 120, diastolic: 75, timestamp: new Date() },
|
||||||
|
heartRate: { value: 72, timestamp: new Date() },
|
||||||
|
temperature: { value: 37.0, timestamp: new Date() },
|
||||||
|
respiratoryRate: { value: 16, timestamp: new Date() },
|
||||||
|
oxygenSaturation: { value: 99, timestamp: new Date() },
|
||||||
|
},
|
||||||
|
medicalHistory: [],
|
||||||
|
currentDiagnosis: 'AI Analysis Pending - CT scan uploaded',
|
||||||
lastUpdated: new Date(),
|
lastUpdated: new Date(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -323,31 +384,31 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
/**
|
/**
|
||||||
* generateMockAlerts Function
|
* generateMockAlerts Function
|
||||||
*
|
*
|
||||||
* Purpose: Generate mock alert data for demonstration
|
* Purpose: Generate mock alert data focused on brain AI predictions
|
||||||
*
|
*
|
||||||
* Returns: Array of Alert objects with realistic medical alerts
|
* Returns: Array of Alert objects with brain-related alerts
|
||||||
*/
|
*/
|
||||||
const generateMockAlerts = (): AlertType[] => [
|
const generateMockAlerts = (): AlertType[] => [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
type: 'CRITICAL_FINDING' as const, // Type of alert
|
type: 'CRITICAL_FINDING' as const,
|
||||||
priority: 'CRITICAL' as const, // Priority level
|
priority: 'CRITICAL' as const,
|
||||||
title: 'Critical Finding Detected',
|
title: 'Critical Brain Finding Detected',
|
||||||
message: 'AI has detected a potential brain bleed in CT scan. Immediate review required.',
|
message: 'AI has detected Intraparenchymal hemorrhage (IPH) with 94% confidence. Immediate neurosurgical consultation required.',
|
||||||
patientId: '4', // Associated patient
|
patientId: '1',
|
||||||
patientName: 'Sarah Wilson',
|
patientName: 'John Doe',
|
||||||
bedNumber: 'D4',
|
bedNumber: 'A1',
|
||||||
timestamp: new Date(), // When alert was generated
|
timestamp: new Date(),
|
||||||
isRead: false, // Whether alert has been read
|
isRead: false,
|
||||||
isAcknowledged: false, // Whether alert has been acknowledged
|
isAcknowledged: false,
|
||||||
actionRequired: true, // Whether action is required
|
actionRequired: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
type: 'VITAL_SIGNS_ALERT' as const,
|
type: 'VITAL_SIGNS_ALERT' as const,
|
||||||
priority: 'HIGH' as const,
|
priority: 'HIGH' as const,
|
||||||
title: 'Vital Sign Alert',
|
title: 'Elevated ICP Alert',
|
||||||
message: 'Blood pressure elevated: 180/110. Patient requires immediate attention.',
|
message: 'Patient Sarah Wilson showing signs of elevated intracranial pressure. BP: 190/120, HR: 60. Immediate intervention needed.',
|
||||||
patientId: '4',
|
patientId: '4',
|
||||||
patientName: 'Sarah Wilson',
|
patientName: 'Sarah Wilson',
|
||||||
bedNumber: 'D4',
|
bedNumber: 'D4',
|
||||||
@ -360,16 +421,30 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
id: '3',
|
id: '3',
|
||||||
type: 'MEDICATION_ALERT' as const,
|
type: 'MEDICATION_ALERT' as const,
|
||||||
priority: 'MEDIUM' as const,
|
priority: 'MEDIUM' as const,
|
||||||
title: 'Medication Due',
|
title: 'AI Analysis Complete',
|
||||||
message: 'Morphine due for patient John Doe in 15 minutes.',
|
message: 'Brain CT analysis complete for Emily Johnson. Results: Normal brain - 98% confidence. No intervention required.',
|
||||||
patientId: '1',
|
patientId: '6',
|
||||||
patientName: 'John Doe',
|
patientName: 'Emily Johnson',
|
||||||
bedNumber: 'A1',
|
bedNumber: 'F6',
|
||||||
timestamp: new Date(Date.now() - 600000), // 10 minutes ago
|
timestamp: new Date(Date.now() - 600000), // 10 minutes ago
|
||||||
isRead: true,
|
isRead: true,
|
||||||
isAcknowledged: true,
|
isAcknowledged: true,
|
||||||
actionRequired: false,
|
actionRequired: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
type: 'CRITICAL_FINDING' as const,
|
||||||
|
priority: 'CRITICAL' as const,
|
||||||
|
title: 'Stroke Alert - Time Sensitive',
|
||||||
|
message: 'Michael Brown diagnosed with ischemic stroke. tPA window closing. Immediate thrombolytic therapy required.',
|
||||||
|
patientId: '3',
|
||||||
|
patientName: 'Michael Brown',
|
||||||
|
bedNumber: 'C3',
|
||||||
|
timestamp: new Date(Date.now() - 120000), // 2 minutes ago
|
||||||
|
isRead: false,
|
||||||
|
isAcknowledged: false,
|
||||||
|
actionRequired: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -380,19 +455,13 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
* useEffect for initial data loading
|
* useEffect for initial data loading
|
||||||
*
|
*
|
||||||
* Purpose: Load initial mock data when component mounts
|
* Purpose: Load initial mock data when component mounts
|
||||||
*
|
|
||||||
* Flow:
|
|
||||||
* 1. Set loading state to true
|
|
||||||
* 2. Generate mock data
|
|
||||||
* 3. Set data in state
|
|
||||||
* 4. Set loading state to false
|
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadInitialData = async () => {
|
const loadInitialData = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// Simulate API call delay
|
// Simulate API call delay
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
setTimeout(() => {}, 1000);
|
||||||
|
|
||||||
// Generate and set mock data
|
// Generate and set mock data
|
||||||
setDashboard(generateMockDashboard());
|
setDashboard(generateMockDashboard());
|
||||||
@ -413,25 +482,19 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
* handleRefresh Function
|
* handleRefresh Function
|
||||||
*
|
*
|
||||||
* Purpose: Handle pull-to-refresh functionality to update dashboard data
|
* Purpose: Handle pull-to-refresh functionality to update dashboard data
|
||||||
*
|
|
||||||
* Flow:
|
|
||||||
* 1. Set refreshing state to true (show loading indicator)
|
|
||||||
* 2. Simulate API call with delay
|
|
||||||
* 3. Update data with fresh mock data
|
|
||||||
* 4. Set refreshing state to false (hide loading indicator)
|
|
||||||
*/
|
*/
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true); // Show refresh indicator
|
setRefreshing(true);
|
||||||
|
|
||||||
// Simulate API call with 1-second delay
|
// Simulate API call with 1-second delay
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
setTimeout(() => {}, 1000);
|
||||||
|
|
||||||
// Update data with fresh mock data
|
// Update data with fresh mock data
|
||||||
setDashboard(generateMockDashboard());
|
setDashboard(generateMockDashboard());
|
||||||
setPatients(generateMockPatients());
|
setPatients(generateMockPatients());
|
||||||
setAlerts(generateMockAlerts());
|
setAlerts(generateMockAlerts());
|
||||||
|
|
||||||
setRefreshing(false); // Hide refresh indicator
|
setRefreshing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -442,9 +505,8 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
* @param patient - Patient object that was pressed
|
* @param patient - Patient object that was pressed
|
||||||
*/
|
*/
|
||||||
const handlePatientPress = (patient: Patient) => {
|
const handlePatientPress = (patient: Patient) => {
|
||||||
// TODO: Navigate to patient details screen
|
|
||||||
console.log('Patient pressed:', patient.firstName, patient.lastName);
|
console.log('Patient pressed:', patient.firstName, patient.lastName);
|
||||||
Alert.alert('Patient Details', `Navigate to ${patient.firstName} ${patient.lastName}'s details`);
|
Alert.alert('Brain Analysis Details', `Navigate to ${patient.firstName} ${patient.lastName}'s brain scan analysis`);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -455,9 +517,8 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
* @param alert - Alert object that was pressed
|
* @param alert - Alert object that was pressed
|
||||||
*/
|
*/
|
||||||
const handleAlertPress = (alert: AlertType) => {
|
const handleAlertPress = (alert: AlertType) => {
|
||||||
// TODO: Navigate to alert details or patient details
|
|
||||||
console.log('Alert pressed:', alert.title);
|
console.log('Alert pressed:', alert.title);
|
||||||
Alert.alert('Alert Details', alert.message);
|
Alert.alert('Brain Alert Details', alert.message);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -470,18 +531,18 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
* Purpose: Filter patients based on selected filter criteria
|
* Purpose: Filter patients based on selected filter criteria
|
||||||
*
|
*
|
||||||
* Filter Options:
|
* Filter Options:
|
||||||
* - 'all': Show all patients regardless of status or priority
|
* - 'all': Show all brain scan cases
|
||||||
* - 'critical': Show only patients with CRITICAL priority
|
* - 'critical': Show only cases with CRITICAL priority
|
||||||
* - 'active': Show only patients with ACTIVE status
|
* - 'pending': Show only cases with PENDING status (awaiting AI analysis)
|
||||||
*/
|
*/
|
||||||
const filteredPatients = patients.filter(patient => {
|
const filteredPatients = patients.filter(patient => {
|
||||||
switch (selectedFilter) {
|
switch (selectedFilter) {
|
||||||
case 'critical':
|
case 'critical':
|
||||||
return patient.priority === 'CRITICAL'; // Only critical priority patients
|
return patient.priority === 'CRITICAL';
|
||||||
case 'active':
|
case 'pending':
|
||||||
return patient.status === 'ACTIVE'; // Only active status patients
|
return patient.status === 'PENDING';
|
||||||
default:
|
default:
|
||||||
return true; // All patients
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -489,21 +550,15 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
* criticalAlerts - Computed property
|
* criticalAlerts - Computed property
|
||||||
*
|
*
|
||||||
* Purpose: Extract critical alerts from all alerts for immediate display
|
* Purpose: Extract critical alerts from all alerts for immediate display
|
||||||
*
|
|
||||||
* Filter: Only alerts with CRITICAL priority that require immediate attention
|
|
||||||
*/
|
*/
|
||||||
const criticalAlerts = alerts.filter(alert => alert.priority === 'CRITICAL');
|
const criticalAlerts = alerts.filter(alert => alert.priority === 'CRITICAL');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* pendingScans - Computed property
|
* pendingScans - Computed property
|
||||||
*
|
*
|
||||||
* Purpose: Identify patients with pending scans or high priority needs
|
* Purpose: Identify patients with pending AI analysis
|
||||||
*
|
|
||||||
* Filter: Patients with PENDING status or HIGH priority
|
|
||||||
*/
|
*/
|
||||||
const pendingScans = patients.filter(patient =>
|
const pendingScans = patients.filter(patient => patient.status === 'PENDING');
|
||||||
patient.status === 'PENDING' || patient.priority === 'HIGH'
|
|
||||||
);
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// RENDER FUNCTIONS
|
// RENDER FUNCTIONS
|
||||||
@ -520,7 +575,7 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
const renderPatientCard = ({ item }: { item: Patient }) => (
|
const renderPatientCard = ({ item }: { item: Patient }) => (
|
||||||
<PatientCard
|
<PatientCard
|
||||||
patient={item}
|
patient={item}
|
||||||
onPress={() => handlePatientPress(item)} // Navigate to patient details
|
onPress={() => handlePatientPress(item)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -528,13 +583,6 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
* renderHeader Function
|
* renderHeader Function
|
||||||
*
|
*
|
||||||
* Purpose: Render the dashboard header section with all dashboard components
|
* Purpose: Render the dashboard header section with all dashboard components
|
||||||
*
|
|
||||||
* Components included:
|
|
||||||
* - DashboardHeader: Shows shift info and key statistics
|
|
||||||
* - CriticalAlerts: Displays urgent alerts requiring attention
|
|
||||||
* - QuickActions: Provides quick access to common functions
|
|
||||||
* - DepartmentStats: Shows department-wise patient distribution
|
|
||||||
* - Filter buttons: Allow filtering of patient list
|
|
||||||
*/
|
*/
|
||||||
const renderHeader = () => (
|
const renderHeader = () => (
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
@ -545,26 +593,25 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
{criticalAlerts.length > 0 && (
|
{criticalAlerts.length > 0 && (
|
||||||
<CriticalAlerts
|
<CriticalAlerts
|
||||||
alerts={criticalAlerts}
|
alerts={criticalAlerts}
|
||||||
onAlertPress={handleAlertPress} // Handle alert interaction
|
onAlertPress={handleAlertPress}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Quick action buttons for common tasks */}
|
{/* Quick action buttons for brain imaging tasks */}
|
||||||
<QuickActions
|
<QuickActions
|
||||||
onQuickAction={(action) => {
|
onQuickAction={(action) => {
|
||||||
// TODO: Implement quick action handlers
|
|
||||||
console.log('Quick action:', action);
|
console.log('Quick action:', action);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Department statistics showing patient distribution */}
|
{/* Department statistics showing brain case distribution */}
|
||||||
{dashboard && <DepartmentStats stats={dashboard.departmentStats} />}
|
{dashboard && <BrainPredictionsOverview dashboard={dashboard} />}
|
||||||
|
|
||||||
{/* Patient filter section with filter buttons */}
|
{/* Brain case filter section with filter buttons */}
|
||||||
<View style={styles.filterContainer}>
|
<View style={styles.filterContainer}>
|
||||||
<Text style={styles.sectionTitle}>Patients</Text>
|
<Text style={styles.sectionTitle}>Brain Scan Cases</Text>
|
||||||
<View style={styles.filterButtons}>
|
<View style={styles.filterButtons}>
|
||||||
{/* All patients filter button */}
|
{/* All cases filter button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.filterButton,
|
styles.filterButton,
|
||||||
@ -580,7 +627,7 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Critical patients filter button */}
|
{/* Critical cases filter button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.filterButton,
|
styles.filterButton,
|
||||||
@ -596,19 +643,19 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Active patients filter button */}
|
{/* Pending AI analysis filter button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.filterButton,
|
styles.filterButton,
|
||||||
selectedFilter === 'active' && styles.filterButtonActive
|
selectedFilter === 'pending' && styles.filterButtonActive
|
||||||
]}
|
]}
|
||||||
onPress={() => setSelectedFilter('active')}
|
onPress={() => setSelectedFilter('pending')}
|
||||||
>
|
>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.filterButtonText,
|
styles.filterButtonText,
|
||||||
selectedFilter === 'active' && styles.filterButtonTextActive
|
selectedFilter === 'pending' && styles.filterButtonTextActive
|
||||||
]}>
|
]}>
|
||||||
Active ({patients.filter(p => p.status === 'ACTIVE').length})
|
Pending AI ({patients.filter(p => p.status === 'PENDING').length})
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@ -628,7 +675,7 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<Text style={styles.loadingText}>Loading Dashboard...</Text>
|
<Text style={styles.loadingText}>Loading Brain AI Dashboard...</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -641,20 +688,20 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
|||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{/* FlatList for efficient rendering of patient cards */}
|
{/* FlatList for efficient rendering of patient cards */}
|
||||||
<FlatList
|
<FlatList
|
||||||
data={filteredPatients} // Filtered patient data
|
data={filteredPatients}
|
||||||
renderItem={renderPatientCard} // Render function for each patient
|
renderItem={renderPatientCard}
|
||||||
keyExtractor={(item) => item.id} // Unique key for each patient
|
keyExtractor={(item) => item.id}
|
||||||
ListHeaderComponent={renderHeader} // Header with dashboard components
|
ListHeaderComponent={renderHeader}
|
||||||
contentContainerStyle={styles.listContainer} // Container styling
|
contentContainerStyle={styles.listContainer}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing} // Show refresh indicator
|
refreshing={refreshing}
|
||||||
onRefresh={handleRefresh} // Handle refresh action
|
onRefresh={handleRefresh}
|
||||||
colors={[theme.colors.primary]} // Refresh indicator color
|
colors={[theme.colors.primary]}
|
||||||
tintColor={theme.colors.primary} // iOS refresh indicator color
|
tintColor={theme.colors.primary}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
showsVerticalScrollIndicator={false} // Hide scroll indicator for cleaner look
|
showsVerticalScrollIndicator={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@ -688,18 +735,18 @@ const styles = StyleSheet.create({
|
|||||||
|
|
||||||
// Container for the FlatList content
|
// Container for the FlatList content
|
||||||
listContainer: {
|
listContainer: {
|
||||||
paddingBottom: theme.spacing.lg, // Add bottom padding for tab bar
|
paddingBottom: theme.spacing.lg,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Header section containing dashboard components
|
// Header section containing dashboard components
|
||||||
header: {
|
header: {
|
||||||
paddingHorizontal: theme.spacing.md, // Horizontal padding for content
|
paddingHorizontal: theme.spacing.md,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Container for patient filter section
|
// Container for patient filter section
|
||||||
filterContainer: {
|
filterContainer: {
|
||||||
marginTop: theme.spacing.lg, // Top margin for separation
|
marginTop: theme.spacing.lg,
|
||||||
marginBottom: theme.spacing.md, // Bottom margin for list spacing
|
marginBottom: theme.spacing.md,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Section title styling
|
// Section title styling
|
||||||
@ -712,8 +759,8 @@ const styles = StyleSheet.create({
|
|||||||
|
|
||||||
// Container for filter buttons
|
// Container for filter buttons
|
||||||
filterButtons: {
|
filterButtons: {
|
||||||
flexDirection: 'row', // Horizontal layout
|
flexDirection: 'row',
|
||||||
gap: theme.spacing.sm, // Space between buttons
|
gap: theme.spacing.sm,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Individual filter button styling
|
// Individual filter button styling
|
||||||
@ -728,8 +775,8 @@ const styles = StyleSheet.create({
|
|||||||
|
|
||||||
// Active filter button styling
|
// Active filter button styling
|
||||||
filterButtonActive: {
|
filterButtonActive: {
|
||||||
backgroundColor: theme.colors.primary, // Primary color background
|
backgroundColor: theme.colors.primary,
|
||||||
borderColor: theme.colors.primary, // Primary color border
|
borderColor: theme.colors.primary,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Filter button text styling
|
// Filter button text styling
|
||||||
@ -741,7 +788,7 @@ const styles = StyleSheet.create({
|
|||||||
|
|
||||||
// Active filter button text styling
|
// Active filter button text styling
|
||||||
filterButtonTextActive: {
|
filterButtonTextActive: {
|
||||||
color: theme.colors.background, // White text on primary background
|
color: theme.colors.background,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -445,7 +445,7 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
|||||||
{user.display_name || `${user.first_name} ${user.last_name}`}
|
{user.display_name || `${user.first_name} ${user.last_name}`}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.profileEmail}>{user.email}</Text>
|
<Text style={styles.profileEmail}>{user.email}</Text>
|
||||||
<Text style={styles.profileRole}>{user.dashboard_role}</Text>
|
<Text style={styles.profileRole}>Physician</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.editIcon}>
|
<View style={styles.editIcon}>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { DashboardStackNavigator } from '../modules/Dashboard/navigation';
|
|||||||
import { SettingsStackNavigator } from '../modules/Settings/navigation';
|
import { SettingsStackNavigator } from '../modules/Settings/navigation';
|
||||||
import { MainTabParamList } from './navigationTypes';
|
import { MainTabParamList } from './navigationTypes';
|
||||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||||
|
import { ComingSoonScreen } from '../shared/components';
|
||||||
|
|
||||||
// Create the bottom tab navigator
|
// Create the bottom tab navigator
|
||||||
const Tab = createBottomTabNavigator<MainTabParamList>();
|
const Tab = createBottomTabNavigator<MainTabParamList>();
|
||||||
@ -82,7 +83,7 @@ export const MainTabNavigator: React.FC = () => {
|
|||||||
{/* Patients Tab - Patient list and management */}
|
{/* Patients Tab - Patient list and management */}
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="Patients"
|
name="Patients"
|
||||||
component={DashboardStackNavigator} // TODO: Replace with actual PatientsScreen
|
component={ComingSoonScreen} // TODO: Replace with actual PatientsScreen
|
||||||
options={{
|
options={{
|
||||||
title: 'Patient List',
|
title: 'Patient List',
|
||||||
tabBarLabel: 'Patients',
|
tabBarLabel: 'Patients',
|
||||||
@ -97,7 +98,7 @@ export const MainTabNavigator: React.FC = () => {
|
|||||||
{/* Alerts Tab - Critical notifications */}
|
{/* Alerts Tab - Critical notifications */}
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="Alerts"
|
name="Alerts"
|
||||||
component={DashboardStackNavigator} // TODO: Replace with actual AlertsScreen
|
component={ComingSoonScreen} // TODO: Replace with actual AlertsScreen
|
||||||
options={{
|
options={{
|
||||||
title: 'Alerts',
|
title: 'Alerts',
|
||||||
tabBarLabel: 'Alerts',
|
tabBarLabel: 'Alerts',
|
||||||
@ -110,7 +111,7 @@ export const MainTabNavigator: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Reports Tab - Medical documentation */}
|
{/* Reports Tab - Medical documentation */}
|
||||||
<Tab.Screen
|
{/* <Tab.Screen
|
||||||
name="Reports"
|
name="Reports"
|
||||||
component={DashboardStackNavigator} // TODO: Replace with actual ReportsScreen
|
component={DashboardStackNavigator} // TODO: Replace with actual ReportsScreen
|
||||||
options={{
|
options={{
|
||||||
@ -122,7 +123,7 @@ export const MainTabNavigator: React.FC = () => {
|
|||||||
),
|
),
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
}}
|
}}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
{/* Settings Tab - User preferences */}
|
{/* Settings Tab - User preferences */}
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
|
|||||||
378
app/shared/components/ComingSoonScreen.tsx
Normal file
378
app/shared/components/ComingSoonScreen.tsx
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
/*
|
||||||
|
* File: ComingSoonScreen.tsx
|
||||||
|
* Description: Coming Soon screen component with SVG image placeholder
|
||||||
|
* Design & Developed by Tech4Biz Solutions
|
||||||
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
ScrollView,
|
||||||
|
TouchableOpacity,
|
||||||
|
SafeAreaView,
|
||||||
|
StatusBar,
|
||||||
|
Image,
|
||||||
|
} from 'react-native';
|
||||||
|
import { theme } from '../../theme/theme';
|
||||||
|
import Icon from 'react-native-vector-icons/Feather';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// INTERFACES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ComingSoonScreenProps Interface
|
||||||
|
*
|
||||||
|
* Purpose: Defines the props required by the ComingSoonScreen component
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - title: Main title for the coming soon screen (optional)
|
||||||
|
* - subtitle: Subtitle or description text (optional)
|
||||||
|
* - onBack: Function to handle back navigation (optional)
|
||||||
|
* - showBackButton: Whether to show back button (default: true)
|
||||||
|
* - backgroundColor: Custom background color (optional)
|
||||||
|
* - textColor: Custom text color (optional)
|
||||||
|
* - imageComponent: Custom SVG/image component to render (optional)
|
||||||
|
*/
|
||||||
|
interface ComingSoonScreenProps {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
onBack?: () => void;
|
||||||
|
showBackButton?: boolean;
|
||||||
|
backgroundColor?: string;
|
||||||
|
textColor?: string;
|
||||||
|
imageComponent?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// COMING SOON SCREEN COMPONENT
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ComingSoonScreen Component
|
||||||
|
*
|
||||||
|
* Purpose: Displays a coming soon message with space for SVG image
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* 1. Customizable title and subtitle
|
||||||
|
* 2. Space allocated for SVG image
|
||||||
|
* 3. Optional back button with navigation
|
||||||
|
* 4. Customizable colors and styling
|
||||||
|
* 5. Responsive design for different screen sizes
|
||||||
|
* 6. Safe area handling
|
||||||
|
* 7. Status bar management
|
||||||
|
* 8. Scrollable content for smaller screens
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - For features under development
|
||||||
|
* - Placeholder screens during app development
|
||||||
|
* - Maintenance or update screens
|
||||||
|
* - Feature announcements
|
||||||
|
*/
|
||||||
|
export const ComingSoonScreen: React.FC<ComingSoonScreenProps> = ({
|
||||||
|
title = 'Coming Soon',
|
||||||
|
subtitle = 'We\'re working hard to bring you something amazing. Stay tuned for updates!',
|
||||||
|
onBack,
|
||||||
|
showBackButton = true,
|
||||||
|
backgroundColor,
|
||||||
|
textColor,
|
||||||
|
imageComponent,
|
||||||
|
}) => {
|
||||||
|
// ============================================================================
|
||||||
|
// HELPER FUNCTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handleBack Function
|
||||||
|
*
|
||||||
|
* Purpose: Handle back navigation if onBack function is provided
|
||||||
|
*/
|
||||||
|
const handleBack = () => {
|
||||||
|
if (onBack) {
|
||||||
|
onBack();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// RENDER SECTION
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={[
|
||||||
|
styles.container,
|
||||||
|
backgroundColor && { backgroundColor }
|
||||||
|
]}>
|
||||||
|
<StatusBar
|
||||||
|
barStyle="dark-content"
|
||||||
|
backgroundColor={backgroundColor || theme.colors.background}
|
||||||
|
translucent={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
style={styles.scrollView}
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
bounces={false}
|
||||||
|
>
|
||||||
|
{/* Header with Back Button */}
|
||||||
|
{showBackButton && onBack && (
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={handleBack}
|
||||||
|
style={styles.backButton}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="arrow-left"
|
||||||
|
size={24}
|
||||||
|
color={textColor || theme.colors.textPrimary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<View style={styles.content}>
|
||||||
|
{/* SVG Image Container */}
|
||||||
|
<View style={styles.imageContainer}>
|
||||||
|
{imageComponent ? (
|
||||||
|
imageComponent
|
||||||
|
) : (
|
||||||
|
<View style={styles.imagePlaceholder}>
|
||||||
|
<Image
|
||||||
|
source={require('../../assets/images/coming-soon.jpg')}
|
||||||
|
style={styles.imagePlaceholder}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<Text style={[
|
||||||
|
styles.title,
|
||||||
|
textColor && { color: textColor }
|
||||||
|
]}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Subtitle */}
|
||||||
|
<Text style={[
|
||||||
|
styles.subtitle,
|
||||||
|
textColor && { color: textColor }
|
||||||
|
]}>
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<View style={styles.buttonContainer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.button,
|
||||||
|
styles.primaryButton,
|
||||||
|
{ backgroundColor: textColor || theme.colors.primary }
|
||||||
|
]}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<Text style={[
|
||||||
|
styles.buttonText,
|
||||||
|
{ color: backgroundColor || theme.colors.background }
|
||||||
|
]}>
|
||||||
|
Get Notified
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.button,
|
||||||
|
styles.secondaryButton,
|
||||||
|
{ borderColor: textColor || theme.colors.primary }
|
||||||
|
]}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<Text style={[
|
||||||
|
styles.secondaryButtonText,
|
||||||
|
{ color: textColor || theme.colors.primary }
|
||||||
|
]}>
|
||||||
|
Learn More
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STYLES SECTION
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
// Main container
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Scroll view
|
||||||
|
scrollView: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Scroll content
|
||||||
|
scrollContent: {
|
||||||
|
flexGrow: 1,
|
||||||
|
paddingHorizontal: theme.spacing.lg,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Header section
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingTop: theme.spacing.lg,
|
||||||
|
paddingBottom: theme.spacing.md,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
backButton: {
|
||||||
|
padding: theme.spacing.sm,
|
||||||
|
borderRadius: theme.borderRadius.medium,
|
||||||
|
backgroundColor: theme.colors.backgroundAlt,
|
||||||
|
...theme.shadows.small,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Main content
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: theme.spacing.xxl,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Image container
|
||||||
|
imageContainer: {
|
||||||
|
width: 280,
|
||||||
|
height: 280,
|
||||||
|
borderRadius: theme.borderRadius.large,
|
||||||
|
backgroundColor: theme.colors.backgroundAlt,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: theme.spacing.xl,
|
||||||
|
...theme.shadows.medium,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Image placeholder
|
||||||
|
imagePlaceholder: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Placeholder text
|
||||||
|
placeholderText: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyMedium,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
marginTop: theme.spacing.sm,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Title
|
||||||
|
title: {
|
||||||
|
fontSize: theme.typography.fontSize.displayLarge,
|
||||||
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
|
color: theme.colors.textPrimary,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: theme.spacing.md,
|
||||||
|
lineHeight: theme.typography.fontSize.displayLarge * 1.2,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Subtitle
|
||||||
|
subtitle: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyLarge,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: theme.spacing.xl,
|
||||||
|
lineHeight: theme.typography.fontSize.bodyLarge * 1.4,
|
||||||
|
paddingHorizontal: theme.spacing.md,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Info container
|
||||||
|
infoContainer: {
|
||||||
|
marginBottom: theme.spacing.xl,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Info item
|
||||||
|
infoItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: theme.spacing.sm,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Info text
|
||||||
|
infoText: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyMedium,
|
||||||
|
fontFamily: theme.typography.fontFamily.medium,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
marginLeft: theme.spacing.sm,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Button container
|
||||||
|
buttonContainer: {
|
||||||
|
width: '100%',
|
||||||
|
gap: theme.spacing.md,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Base button
|
||||||
|
button: {
|
||||||
|
paddingVertical: theme.spacing.md,
|
||||||
|
paddingHorizontal: theme.spacing.lg,
|
||||||
|
borderRadius: theme.borderRadius.medium,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
minHeight: 48,
|
||||||
|
...theme.shadows.primary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Primary button
|
||||||
|
primaryButton: {
|
||||||
|
backgroundColor: theme.colors.primary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Secondary button
|
||||||
|
secondaryButton: {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: theme.colors.primary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Button text
|
||||||
|
buttonText: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyLarge,
|
||||||
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
|
color: theme.colors.background,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Secondary button text
|
||||||
|
secondaryButtonText: {
|
||||||
|
fontSize: theme.typography.fontSize.bodyLarge,
|
||||||
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
|
color: theme.colors.primary,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ComingSoonScreen;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* End of File: ComingSoonScreen.tsx
|
||||||
|
* Design & Developed by Tech4Biz Solutions
|
||||||
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||||
|
*/
|
||||||
@ -5,7 +5,22 @@
|
|||||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { CustomModal } from './CustomModal';
|
// ============================================================================
|
||||||
|
// SHARED COMPONENTS EXPORTS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Custom Modal Component
|
||||||
|
export { CustomModal } from './CustomModal';
|
||||||
|
|
||||||
|
// Coming Soon Screen Component
|
||||||
|
export { ComingSoonScreen } from './ComingSoonScreen';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TYPE EXPORTS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Export types for components
|
||||||
|
// export type { CustomModalProps } from './CustomModal';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* End of File: index.ts
|
* End of File: index.ts
|
||||||
|
|||||||
@ -43,6 +43,7 @@ export interface User {
|
|||||||
onboarding_step: number;
|
onboarding_step: number;
|
||||||
onboarding_message: string;
|
onboarding_message: string;
|
||||||
access_token: string;
|
access_token: string;
|
||||||
|
platform: 'app'|'web';
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserRole =
|
export type UserRole =
|
||||||
|
|||||||
@ -58,5 +58,8 @@
|
|||||||
<string>Roboto-Regular.ttf</string>
|
<string>Roboto-Regular.ttf</string>
|
||||||
<string>Roboto-SemiBold.ttf</string>
|
<string>Roboto-SemiBold.ttf</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>This app needs access to the camera to take photos and videos.</string>
|
||||||
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@ -11968,9 +11968,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-native-svg": {
|
"node_modules/react-native-svg": {
|
||||||
"version": "15.12.0",
|
"version": "15.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz",
|
||||||
"integrity": "sha512-iE25PxIJ6V0C6krReLquVw6R0QTsRTmEQc4K2Co3P6zsimU/jltcDBKYDy1h/5j9S/fqmMeXnpM+9LEWKJKI6A==",
|
"integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"css-select": "^5.1.0",
|
"css-select": "^5.1.0",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user