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">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">NeoScan_Physician</string>
|
||||
<string name="app_name">NeoScanPhysician</string>
|
||||
</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}>
|
||||
<Text style={styles.sectionTitle}>Upload your ID document</Text>
|
||||
<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>
|
||||
|
||||
{/* Document Upload Area */}
|
||||
|
||||
@ -313,7 +313,7 @@ const styles = StyleSheet.create({
|
||||
|
||||
// Eye icon for password visibility
|
||||
eyeIcon: {
|
||||
paddingLeft: theme.spacing.sm,
|
||||
padding: theme.spacing.sm,
|
||||
},
|
||||
|
||||
// Base button styling
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
@ -16,7 +16,15 @@ import {
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
Alert,
|
||||
Image,
|
||||
PermissionsAndroid,
|
||||
} from 'react-native';
|
||||
import {
|
||||
launchImageLibrary,
|
||||
launchCamera,
|
||||
ImagePickerResponse,
|
||||
MediaType,
|
||||
} from 'react-native-image-picker';
|
||||
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
||||
import { updateOnboarded, logout } from '../redux/authSlice';
|
||||
import { authAPI } from '../services/authAPI';
|
||||
@ -49,18 +57,41 @@ interface PasswordRule {
|
||||
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
|
||||
*
|
||||
* Purpose: Password reset screen for users who need to set their initial password
|
||||
*
|
||||
* Features:
|
||||
* 1. Password and confirm password input fields
|
||||
* 2. Real-time password validation with visual feedback
|
||||
* 3. Password visibility toggles
|
||||
* 4. Password strength requirements display
|
||||
* 5. Integration with Redux for onboarding status
|
||||
* 6. API integration for password change
|
||||
* 1. Two flows based on platform:
|
||||
* - platform = 'web': Document upload first, then password reset
|
||||
* - platform = 'app': Password reset only
|
||||
* 2. Document upload with camera/gallery selection
|
||||
* 3. Password and confirm password input fields
|
||||
* 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:
|
||||
* - At least 8 characters
|
||||
@ -84,6 +115,13 @@ export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
|
||||
const [isConfirmPasswordVisible, setConfirmPasswordVisible] = 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
|
||||
const dispatch = useAppDispatch();
|
||||
const user = useAppSelector((state) => state.auth.user);
|
||||
@ -141,6 +179,19 @@ export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
|
||||
updatePasswordRules(password);
|
||||
}, [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
|
||||
// ============================================================================
|
||||
@ -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
|
||||
// ============================================================================
|
||||
@ -319,6 +601,191 @@ export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
|
||||
</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
|
||||
// ============================================================================
|
||||
@ -338,97 +805,33 @@ export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
|
||||
<TouchableOpacity onPress={handleBack} style={styles.backButton}>
|
||||
<Icon name="log-out" size={24} color={theme.colors.textPrimary} />
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
{/* Step Indicator */}
|
||||
{user?.platform === 'web' && (
|
||||
<View style={styles.stepIndicator}>
|
||||
<View style={styles.stepContainer}>
|
||||
<View style={[styles.stepCircle, currentStep === 'document' && styles.stepCircleActive]}>
|
||||
<Text style={[styles.stepNumber, currentStep === 'document' && styles.stepNumberActive]}>1</Text>
|
||||
</View>
|
||||
<Text style={[styles.stepLabel, currentStep === 'document' && styles.stepLabelActive]}>Document</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.buttonText}>Set Password</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<View style={styles.stepLine} />
|
||||
<View style={styles.stepContainer}>
|
||||
<View style={[styles.stepCircle, currentStep === 'password' && styles.stepCircleActive]}>
|
||||
<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>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
@ -483,6 +886,72 @@ const styles = StyleSheet.create({
|
||||
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
|
||||
iconContainer: {
|
||||
alignItems: 'center',
|
||||
@ -507,6 +976,119 @@ const styles = StyleSheet.create({
|
||||
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
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
@ -607,6 +1189,12 @@ const styles = StyleSheet.create({
|
||||
marginBottom: theme.spacing.md,
|
||||
},
|
||||
|
||||
// Upload button
|
||||
uploadButton: {
|
||||
backgroundColor: theme.colors.primary,
|
||||
...theme.shadows.primary,
|
||||
},
|
||||
|
||||
// Reset button
|
||||
resetButton: {
|
||||
backgroundColor: theme.colors.primary,
|
||||
|
||||
@ -27,7 +27,9 @@ export const authAPI = {
|
||||
//change password
|
||||
changepassword: (payload:{password:string,token:string | undefined}) => api.post('/api/auth/onboarding/change-password', {password:payload.password},buildHeaders({token:payload.token})),
|
||||
//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
|
||||
};
|
||||
|
||||
|
||||
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,
|
||||
padding: theme.spacing.md,
|
||||
marginRight: theme.spacing.md,
|
||||
minWidth: 280,
|
||||
// minWidth: 280,
|
||||
...theme.shadows.small,
|
||||
width:280
|
||||
},
|
||||
alertHeader: {
|
||||
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
|
||||
* 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
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
@ -22,7 +22,7 @@ import { PatientCard } from '../components/PatientCard';
|
||||
import { CriticalAlerts } from '../components/CriticalAlerts';
|
||||
import { DashboardHeader } from '../components/DashboardHeader';
|
||||
import { QuickActions } from '../components/QuickActions';
|
||||
import { DepartmentStats } from '../components/DepartmentStats';
|
||||
import { BrainPredictionsOverview } from '../components/BrainPredictionsOverview';
|
||||
|
||||
/**
|
||||
* ERDashboardScreenProps Interface
|
||||
@ -36,30 +36,52 @@ interface ERDashboardScreenProps {
|
||||
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
|
||||
*
|
||||
* Purpose: Main dashboard screen for Emergency Department physicians
|
||||
* Purpose: Brain AI Analysis Dashboard for Emergency Department physicians
|
||||
*
|
||||
* Dashboard Features:
|
||||
* 1. Real-time patient overview with filtering capabilities
|
||||
* 2. Critical alerts display for immediate attention
|
||||
* 3. Quick action buttons for common tasks
|
||||
* 4. Department statistics and bed occupancy
|
||||
* 1. Real-time brain scan analysis with AI predictions
|
||||
* 2. Critical neurological alerts for immediate attention
|
||||
* 3. Quick action buttons for brain imaging tasks
|
||||
* 4. Department statistics focused on neurological cases
|
||||
* 5. Pull-to-refresh functionality for live updates
|
||||
* 6. Internal data generation for demonstration
|
||||
* 6. AI prediction confidence scores and analysis
|
||||
*
|
||||
* Patient Filtering:
|
||||
* - All: Shows all patients in the system
|
||||
* - Critical: Shows only patients with critical priority
|
||||
* - Active: Shows only patients with active status
|
||||
* Brain Condition Filtering:
|
||||
* - All: Shows all brain scan cases
|
||||
* - Critical: Shows only cases with critical brain conditions
|
||||
* - Pending: Shows cases awaiting AI analysis
|
||||
*
|
||||
* Data Flow:
|
||||
* 1. Generate mock data internally for demonstration
|
||||
* 2. Filter patients based on selected filter
|
||||
* 3. Display critical alerts at the top
|
||||
* 4. Show patient cards in scrollable list
|
||||
* 5. Handle refresh and navigation interactions
|
||||
* AI Prediction Classes:
|
||||
* - 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
|
||||
*/
|
||||
export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
navigation,
|
||||
@ -71,8 +93,8 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
// Refresh state for pull-to-refresh functionality
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// Patient filter state to control which patients are displayed
|
||||
const [selectedFilter, setSelectedFilter] = useState<'all' | 'critical' | 'active'>('all');
|
||||
// Patient filter state to control which brain cases are displayed
|
||||
const [selectedFilter, setSelectedFilter] = useState<'all' | 'critical' | 'pending'>('all');
|
||||
|
||||
// Dashboard data state
|
||||
const [dashboard, setDashboard] = useState<ERDashboard | null>(null);
|
||||
@ -87,31 +109,31 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
/**
|
||||
* 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 => ({
|
||||
totalPatients: 24, // Total number of patients in ER
|
||||
criticalPatients: 3, // Number of patients requiring immediate attention
|
||||
pendingScans: 8, // Number of scans waiting for review
|
||||
recentReports: 12, // Number of reports generated recently
|
||||
bedOccupancy: 85, // Percentage of beds currently occupied
|
||||
totalPatients: 18, // Total number of brain scan cases
|
||||
criticalPatients: 5, // Number of critical brain conditions
|
||||
pendingScans: 6, // Number of scans awaiting AI analysis
|
||||
recentReports: 12, // Number of AI analysis reports generated
|
||||
bedOccupancy: 78, // Percentage of neurological beds occupied
|
||||
departmentStats: {
|
||||
emergency: 8, // Patients in emergency department
|
||||
trauma: 4, // Patients in trauma department
|
||||
cardiac: 3, // Patients in cardiac department
|
||||
neurology: 2, // Patients in neurology department
|
||||
pediatrics: 5, // Patients in pediatrics department
|
||||
icu: 2, // Patients in ICU
|
||||
emergency: 8, // Emergency brain cases
|
||||
trauma: 4, // Traumatic brain injury cases
|
||||
cardiac: 3, // Stroke cases (using cardiac as placeholder)
|
||||
neurology: 2, // General neurology cases
|
||||
pediatrics: 1, // Neurosurgery cases (using pediatrics as placeholder)
|
||||
icu: 0, // ICU brain cases
|
||||
},
|
||||
shiftInfo: {
|
||||
currentShift: 'DAY' as const, // Current shift (DAY/NIGHT)
|
||||
startTime: new Date(), // Shift start time
|
||||
endTime: new Date(), // Shift end time
|
||||
attendingPhysician: 'Dr. Smith', // Lead physician on duty
|
||||
residents: ['Dr. Johnson', 'Dr. Williams'], // Resident physicians
|
||||
nurses: ['Nurse Brown', 'Nurse Davis'], // Nursing staff
|
||||
attendingPhysician: 'Dr. Smith', // Lead neurologist on duty
|
||||
residents: ['Dr. Johnson', 'Dr. Williams'], // Neurology residents
|
||||
nurses: ['Nurse Brown', 'Nurse Davis'], // Neurology nursing staff
|
||||
},
|
||||
lastUpdated: new Date(), // Last time dashboard was updated
|
||||
});
|
||||
@ -119,30 +141,30 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
/**
|
||||
* 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[] => [
|
||||
{
|
||||
id: '1', // Unique patient identifier
|
||||
mrn: 'MRN001', // Medical Record Number
|
||||
id: '1',
|
||||
mrn: 'MRN001',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
dateOfBirth: new Date('1985-03-15'),
|
||||
gender: 'MALE' as const,
|
||||
age: 38,
|
||||
bedNumber: 'A1', // Assigned bed number
|
||||
roomNumber: '101', // Room number
|
||||
bedNumber: 'A1',
|
||||
roomNumber: '101',
|
||||
admissionDate: new Date('2024-01-15'),
|
||||
status: 'ACTIVE' as const, // Current patient status
|
||||
priority: 'CRITICAL' as const, // Priority level for treatment
|
||||
status: 'ACTIVE' as const,
|
||||
priority: 'CRITICAL' as const,
|
||||
department: 'Emergency',
|
||||
attendingPhysician: 'Dr. Smith',
|
||||
allergies: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Penicillin',
|
||||
name: 'Contrast Dye',
|
||||
severity: 'SEVERE' as const,
|
||||
reaction: 'Anaphylaxis'
|
||||
},
|
||||
@ -150,24 +172,24 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
medications: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Morphine',
|
||||
dosage: '2mg',
|
||||
frequency: 'Every 4 hours',
|
||||
route: 'IV', // Administration route
|
||||
name: 'Mannitol',
|
||||
dosage: '100ml',
|
||||
frequency: 'Every 6 hours',
|
||||
route: 'IV',
|
||||
startDate: new Date(),
|
||||
status: 'ACTIVE' as const,
|
||||
prescribedBy: 'Dr. Smith',
|
||||
},
|
||||
],
|
||||
vitalSigns: {
|
||||
bloodPressure: { systolic: 140, diastolic: 90, timestamp: new Date() },
|
||||
bloodPressure: { systolic: 180, diastolic: 110, timestamp: new Date() },
|
||||
heartRate: { value: 95, timestamp: new Date() },
|
||||
temperature: { value: 37.2, timestamp: new Date() },
|
||||
respiratoryRate: { value: 18, timestamp: new Date() },
|
||||
oxygenSaturation: { value: 98, timestamp: new Date() },
|
||||
},
|
||||
medicalHistory: [],
|
||||
currentDiagnosis: 'Chest pain, rule out MI', // Current medical diagnosis
|
||||
currentDiagnosis: 'AI Predicted: Intraparenchymal hemorrhage (IPH) - 94% confidence',
|
||||
lastUpdated: new Date(),
|
||||
},
|
||||
{
|
||||
@ -182,20 +204,31 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
roomNumber: '102',
|
||||
admissionDate: new Date('2024-01-15'),
|
||||
status: 'ACTIVE' as const,
|
||||
priority: 'HIGH' as const,
|
||||
priority: 'CRITICAL' as const,
|
||||
department: 'Trauma',
|
||||
attendingPhysician: 'Dr. Johnson',
|
||||
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: {
|
||||
bloodPressure: { systolic: 120, diastolic: 80, timestamp: new Date() },
|
||||
bloodPressure: { systolic: 160, diastolic: 90, timestamp: new Date() },
|
||||
heartRate: { value: 88, timestamp: new Date() },
|
||||
temperature: { value: 36.8, timestamp: new Date() },
|
||||
respiratoryRate: { value: 16, timestamp: new Date() },
|
||||
oxygenSaturation: { value: 99, timestamp: new Date() },
|
||||
},
|
||||
medicalHistory: [],
|
||||
currentDiagnosis: 'Multiple trauma from MVA', // MVA = Motor Vehicle Accident
|
||||
currentDiagnosis: 'AI Predicted: Subdural hematoma (SDH) with mass effect - 89% confidence',
|
||||
lastUpdated: new Date(),
|
||||
},
|
||||
{
|
||||
@ -210,8 +243,8 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
roomNumber: '103',
|
||||
admissionDate: new Date('2024-01-15'),
|
||||
status: 'PENDING' as const,
|
||||
priority: 'MEDIUM' as const,
|
||||
department: 'Cardiac',
|
||||
priority: 'HIGH' as const,
|
||||
department: 'Stroke',
|
||||
attendingPhysician: 'Dr. Williams',
|
||||
allergies: [
|
||||
{
|
||||
@ -223,25 +256,25 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
],
|
||||
medications: [
|
||||
{
|
||||
id: '2',
|
||||
name: 'Nitroglycerin',
|
||||
dosage: '0.4mg',
|
||||
frequency: 'As needed',
|
||||
route: 'Sublingual',
|
||||
id: '3',
|
||||
name: 'tPA',
|
||||
dosage: '0.9mg/kg',
|
||||
frequency: 'Single dose',
|
||||
route: 'IV',
|
||||
startDate: new Date(),
|
||||
status: 'ACTIVE' as const,
|
||||
prescribedBy: 'Dr. Williams',
|
||||
},
|
||||
],
|
||||
vitalSigns: {
|
||||
bloodPressure: { systolic: 160, diastolic: 100, timestamp: new Date() },
|
||||
bloodPressure: { systolic: 140, diastolic: 85, timestamp: new Date() },
|
||||
heartRate: { value: 110, timestamp: new Date() },
|
||||
temperature: { value: 36.9, timestamp: new Date() },
|
||||
respiratoryRate: { value: 20, timestamp: new Date() },
|
||||
oxygenSaturation: { value: 95, timestamp: new Date() },
|
||||
},
|
||||
medicalHistory: [],
|
||||
currentDiagnosis: 'Hypertension, chest pain',
|
||||
currentDiagnosis: 'AI Predicted: Stroke (ischemic) - 92% confidence',
|
||||
lastUpdated: new Date(),
|
||||
},
|
||||
{
|
||||
@ -257,15 +290,15 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
admissionDate: new Date('2024-01-15'),
|
||||
status: 'ACTIVE' as const,
|
||||
priority: 'CRITICAL' as const,
|
||||
department: 'Neurology',
|
||||
department: 'Neurosurgery',
|
||||
attendingPhysician: 'Dr. Davis',
|
||||
allergies: [],
|
||||
medications: [
|
||||
{
|
||||
id: '3',
|
||||
name: 'Mannitol',
|
||||
dosage: '100ml',
|
||||
frequency: 'Every 6 hours',
|
||||
id: '4',
|
||||
name: 'Phenytoin',
|
||||
dosage: '100mg',
|
||||
frequency: 'Every 8 hours',
|
||||
route: 'IV',
|
||||
startDate: new Date(),
|
||||
status: 'ACTIVE' as const,
|
||||
@ -273,14 +306,14 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
},
|
||||
],
|
||||
vitalSigns: {
|
||||
bloodPressure: { systolic: 180, diastolic: 110, timestamp: new Date() },
|
||||
bloodPressure: { systolic: 190, diastolic: 120, timestamp: new Date() },
|
||||
heartRate: { value: 60, timestamp: new Date() },
|
||||
temperature: { value: 38.5, timestamp: new Date() },
|
||||
respiratoryRate: { value: 12, timestamp: new Date() },
|
||||
oxygenSaturation: { value: 92, timestamp: new Date() },
|
||||
},
|
||||
medicalHistory: [],
|
||||
currentDiagnosis: 'Suspected intracranial hemorrhage',
|
||||
currentDiagnosis: 'AI Predicted: Epidural hematoma (EDH) with midline shift - 96% confidence',
|
||||
lastUpdated: new Date(),
|
||||
},
|
||||
{
|
||||
@ -296,7 +329,7 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
admissionDate: new Date('2024-01-15'),
|
||||
status: 'ACTIVE' as const,
|
||||
priority: 'HIGH' as const,
|
||||
department: 'Pediatrics',
|
||||
department: 'Neurology',
|
||||
attendingPhysician: 'Dr. Brown',
|
||||
allergies: [
|
||||
{
|
||||
@ -308,14 +341,42 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
],
|
||||
medications: [],
|
||||
vitalSigns: {
|
||||
bloodPressure: { systolic: 110, diastolic: 70, timestamp: new Date() },
|
||||
bloodPressure: { systolic: 130, diastolic: 80, timestamp: new Date() },
|
||||
heartRate: { value: 85, timestamp: new Date() },
|
||||
temperature: { value: 37.8, timestamp: new Date() },
|
||||
respiratoryRate: { value: 22, timestamp: new Date() },
|
||||
oxygenSaturation: { value: 97, timestamp: new Date() },
|
||||
},
|
||||
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(),
|
||||
},
|
||||
];
|
||||
@ -323,31 +384,31 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
/**
|
||||
* 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[] => [
|
||||
{
|
||||
id: '1',
|
||||
type: 'CRITICAL_FINDING' as const, // Type of alert
|
||||
priority: 'CRITICAL' as const, // Priority level
|
||||
title: 'Critical Finding Detected',
|
||||
message: 'AI has detected a potential brain bleed in CT scan. Immediate review required.',
|
||||
patientId: '4', // Associated patient
|
||||
patientName: 'Sarah Wilson',
|
||||
bedNumber: 'D4',
|
||||
timestamp: new Date(), // When alert was generated
|
||||
isRead: false, // Whether alert has been read
|
||||
isAcknowledged: false, // Whether alert has been acknowledged
|
||||
actionRequired: true, // Whether action is required
|
||||
type: 'CRITICAL_FINDING' as const,
|
||||
priority: 'CRITICAL' as const,
|
||||
title: 'Critical Brain Finding Detected',
|
||||
message: 'AI has detected Intraparenchymal hemorrhage (IPH) with 94% confidence. Immediate neurosurgical consultation required.',
|
||||
patientId: '1',
|
||||
patientName: 'John Doe',
|
||||
bedNumber: 'A1',
|
||||
timestamp: new Date(),
|
||||
isRead: false,
|
||||
isAcknowledged: false,
|
||||
actionRequired: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'VITAL_SIGNS_ALERT' as const,
|
||||
priority: 'HIGH' as const,
|
||||
title: 'Vital Sign Alert',
|
||||
message: 'Blood pressure elevated: 180/110. Patient requires immediate attention.',
|
||||
title: 'Elevated ICP Alert',
|
||||
message: 'Patient Sarah Wilson showing signs of elevated intracranial pressure. BP: 190/120, HR: 60. Immediate intervention needed.',
|
||||
patientId: '4',
|
||||
patientName: 'Sarah Wilson',
|
||||
bedNumber: 'D4',
|
||||
@ -360,16 +421,30 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
id: '3',
|
||||
type: 'MEDICATION_ALERT' as const,
|
||||
priority: 'MEDIUM' as const,
|
||||
title: 'Medication Due',
|
||||
message: 'Morphine due for patient John Doe in 15 minutes.',
|
||||
patientId: '1',
|
||||
patientName: 'John Doe',
|
||||
bedNumber: 'A1',
|
||||
title: 'AI Analysis Complete',
|
||||
message: 'Brain CT analysis complete for Emily Johnson. Results: Normal brain - 98% confidence. No intervention required.',
|
||||
patientId: '6',
|
||||
patientName: 'Emily Johnson',
|
||||
bedNumber: 'F6',
|
||||
timestamp: new Date(Date.now() - 600000), // 10 minutes ago
|
||||
isRead: true,
|
||||
isAcknowledged: true,
|
||||
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
|
||||
*
|
||||
* 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(() => {
|
||||
const loadInitialData = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
// Simulate API call delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
setTimeout(() => {}, 1000);
|
||||
|
||||
// Generate and set mock data
|
||||
setDashboard(generateMockDashboard());
|
||||
@ -413,25 +482,19 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
* handleRefresh Function
|
||||
*
|
||||
* 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 () => {
|
||||
setRefreshing(true); // Show refresh indicator
|
||||
setRefreshing(true);
|
||||
|
||||
// Simulate API call with 1-second delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
setTimeout(() => {}, 1000);
|
||||
|
||||
// Update data with fresh mock data
|
||||
setDashboard(generateMockDashboard());
|
||||
setPatients(generateMockPatients());
|
||||
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
|
||||
*/
|
||||
const handlePatientPress = (patient: Patient) => {
|
||||
// TODO: Navigate to patient details screen
|
||||
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
|
||||
*/
|
||||
const handleAlertPress = (alert: AlertType) => {
|
||||
// TODO: Navigate to alert details or patient details
|
||||
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
|
||||
*
|
||||
* Filter Options:
|
||||
* - 'all': Show all patients regardless of status or priority
|
||||
* - 'critical': Show only patients with CRITICAL priority
|
||||
* - 'active': Show only patients with ACTIVE status
|
||||
* - 'all': Show all brain scan cases
|
||||
* - 'critical': Show only cases with CRITICAL priority
|
||||
* - 'pending': Show only cases with PENDING status (awaiting AI analysis)
|
||||
*/
|
||||
const filteredPatients = patients.filter(patient => {
|
||||
switch (selectedFilter) {
|
||||
case 'critical':
|
||||
return patient.priority === 'CRITICAL'; // Only critical priority patients
|
||||
case 'active':
|
||||
return patient.status === 'ACTIVE'; // Only active status patients
|
||||
return patient.priority === 'CRITICAL';
|
||||
case 'pending':
|
||||
return patient.status === 'PENDING';
|
||||
default:
|
||||
return true; // All patients
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
@ -489,21 +550,15 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
* criticalAlerts - Computed property
|
||||
*
|
||||
* 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');
|
||||
|
||||
/**
|
||||
* pendingScans - Computed property
|
||||
*
|
||||
* Purpose: Identify patients with pending scans or high priority needs
|
||||
*
|
||||
* Filter: Patients with PENDING status or HIGH priority
|
||||
* Purpose: Identify patients with pending AI analysis
|
||||
*/
|
||||
const pendingScans = patients.filter(patient =>
|
||||
patient.status === 'PENDING' || patient.priority === 'HIGH'
|
||||
);
|
||||
const pendingScans = patients.filter(patient => patient.status === 'PENDING');
|
||||
|
||||
// ============================================================================
|
||||
// RENDER FUNCTIONS
|
||||
@ -520,7 +575,7 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
const renderPatientCard = ({ item }: { item: Patient }) => (
|
||||
<PatientCard
|
||||
patient={item}
|
||||
onPress={() => handlePatientPress(item)} // Navigate to patient details
|
||||
onPress={() => handlePatientPress(item)}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -528,13 +583,6 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
* renderHeader Function
|
||||
*
|
||||
* 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 = () => (
|
||||
<View style={styles.header}>
|
||||
@ -545,26 +593,25 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
{criticalAlerts.length > 0 && (
|
||||
<CriticalAlerts
|
||||
alerts={criticalAlerts}
|
||||
onAlertPress={handleAlertPress} // Handle alert interaction
|
||||
onAlertPress={handleAlertPress}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Quick action buttons for common tasks */}
|
||||
{/* Quick action buttons for brain imaging tasks */}
|
||||
<QuickActions
|
||||
onQuickAction={(action) => {
|
||||
// TODO: Implement quick action handlers
|
||||
console.log('Quick action:', action);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Department statistics showing patient distribution */}
|
||||
{dashboard && <DepartmentStats stats={dashboard.departmentStats} />}
|
||||
{/* Department statistics showing brain case distribution */}
|
||||
{dashboard && <BrainPredictionsOverview dashboard={dashboard} />}
|
||||
|
||||
{/* Patient filter section with filter buttons */}
|
||||
{/* Brain case filter section with filter buttons */}
|
||||
<View style={styles.filterContainer}>
|
||||
<Text style={styles.sectionTitle}>Patients</Text>
|
||||
<Text style={styles.sectionTitle}>Brain Scan Cases</Text>
|
||||
<View style={styles.filterButtons}>
|
||||
{/* All patients filter button */}
|
||||
{/* All cases filter button */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.filterButton,
|
||||
@ -580,7 +627,7 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Critical patients filter button */}
|
||||
{/* Critical cases filter button */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.filterButton,
|
||||
@ -596,19 +643,19 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Active patients filter button */}
|
||||
{/* Pending AI analysis filter button */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.filterButton,
|
||||
selectedFilter === 'active' && styles.filterButtonActive
|
||||
selectedFilter === 'pending' && styles.filterButtonActive
|
||||
]}
|
||||
onPress={() => setSelectedFilter('active')}
|
||||
onPress={() => setSelectedFilter('pending')}
|
||||
>
|
||||
<Text style={[
|
||||
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>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@ -628,7 +675,7 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={styles.loadingText}>Loading Dashboard...</Text>
|
||||
<Text style={styles.loadingText}>Loading Brain AI Dashboard...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@ -641,20 +688,20 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
<View style={styles.container}>
|
||||
{/* FlatList for efficient rendering of patient cards */}
|
||||
<FlatList
|
||||
data={filteredPatients} // Filtered patient data
|
||||
renderItem={renderPatientCard} // Render function for each patient
|
||||
keyExtractor={(item) => item.id} // Unique key for each patient
|
||||
ListHeaderComponent={renderHeader} // Header with dashboard components
|
||||
contentContainerStyle={styles.listContainer} // Container styling
|
||||
data={filteredPatients}
|
||||
renderItem={renderPatientCard}
|
||||
keyExtractor={(item) => item.id}
|
||||
ListHeaderComponent={renderHeader}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing} // Show refresh indicator
|
||||
onRefresh={handleRefresh} // Handle refresh action
|
||||
colors={[theme.colors.primary]} // Refresh indicator color
|
||||
tintColor={theme.colors.primary} // iOS refresh indicator color
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
colors={[theme.colors.primary]}
|
||||
tintColor={theme.colors.primary}
|
||||
/>
|
||||
}
|
||||
showsVerticalScrollIndicator={false} // Hide scroll indicator for cleaner look
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@ -688,18 +735,18 @@ const styles = StyleSheet.create({
|
||||
|
||||
// Container for the FlatList content
|
||||
listContainer: {
|
||||
paddingBottom: theme.spacing.lg, // Add bottom padding for tab bar
|
||||
paddingBottom: theme.spacing.lg,
|
||||
},
|
||||
|
||||
// Header section containing dashboard components
|
||||
header: {
|
||||
paddingHorizontal: theme.spacing.md, // Horizontal padding for content
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
},
|
||||
|
||||
// Container for patient filter section
|
||||
filterContainer: {
|
||||
marginTop: theme.spacing.lg, // Top margin for separation
|
||||
marginBottom: theme.spacing.md, // Bottom margin for list spacing
|
||||
marginTop: theme.spacing.lg,
|
||||
marginBottom: theme.spacing.md,
|
||||
},
|
||||
|
||||
// Section title styling
|
||||
@ -712,8 +759,8 @@ const styles = StyleSheet.create({
|
||||
|
||||
// Container for filter buttons
|
||||
filterButtons: {
|
||||
flexDirection: 'row', // Horizontal layout
|
||||
gap: theme.spacing.sm, // Space between buttons
|
||||
flexDirection: 'row',
|
||||
gap: theme.spacing.sm,
|
||||
},
|
||||
|
||||
// Individual filter button styling
|
||||
@ -728,8 +775,8 @@ const styles = StyleSheet.create({
|
||||
|
||||
// Active filter button styling
|
||||
filterButtonActive: {
|
||||
backgroundColor: theme.colors.primary, // Primary color background
|
||||
borderColor: theme.colors.primary, // Primary color border
|
||||
backgroundColor: theme.colors.primary,
|
||||
borderColor: theme.colors.primary,
|
||||
},
|
||||
|
||||
// Filter button text styling
|
||||
@ -741,7 +788,7 @@ const styles = StyleSheet.create({
|
||||
|
||||
// Active filter button text styling
|
||||
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}`}
|
||||
</Text>
|
||||
<Text style={styles.profileEmail}>{user.email}</Text>
|
||||
<Text style={styles.profileRole}>{user.dashboard_role}</Text>
|
||||
<Text style={styles.profileRole}>Physician</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.editIcon}>
|
||||
|
||||
@ -12,6 +12,7 @@ import { DashboardStackNavigator } from '../modules/Dashboard/navigation';
|
||||
import { SettingsStackNavigator } from '../modules/Settings/navigation';
|
||||
import { MainTabParamList } from './navigationTypes';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import { ComingSoonScreen } from '../shared/components';
|
||||
|
||||
// Create the bottom tab navigator
|
||||
const Tab = createBottomTabNavigator<MainTabParamList>();
|
||||
@ -82,7 +83,7 @@ export const MainTabNavigator: React.FC = () => {
|
||||
{/* Patients Tab - Patient list and management */}
|
||||
<Tab.Screen
|
||||
name="Patients"
|
||||
component={DashboardStackNavigator} // TODO: Replace with actual PatientsScreen
|
||||
component={ComingSoonScreen} // TODO: Replace with actual PatientsScreen
|
||||
options={{
|
||||
title: 'Patient List',
|
||||
tabBarLabel: 'Patients',
|
||||
@ -97,7 +98,7 @@ export const MainTabNavigator: React.FC = () => {
|
||||
{/* Alerts Tab - Critical notifications */}
|
||||
<Tab.Screen
|
||||
name="Alerts"
|
||||
component={DashboardStackNavigator} // TODO: Replace with actual AlertsScreen
|
||||
component={ComingSoonScreen} // TODO: Replace with actual AlertsScreen
|
||||
options={{
|
||||
title: 'Alerts',
|
||||
tabBarLabel: 'Alerts',
|
||||
@ -110,7 +111,7 @@ export const MainTabNavigator: React.FC = () => {
|
||||
/>
|
||||
|
||||
{/* Reports Tab - Medical documentation */}
|
||||
<Tab.Screen
|
||||
{/* <Tab.Screen
|
||||
name="Reports"
|
||||
component={DashboardStackNavigator} // TODO: Replace with actual ReportsScreen
|
||||
options={{
|
||||
@ -122,7 +123,7 @@ export const MainTabNavigator: React.FC = () => {
|
||||
),
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
{/* Settings Tab - User preferences */}
|
||||
<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.
|
||||
*/
|
||||
|
||||
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
|
||||
|
||||
@ -43,6 +43,7 @@ export interface User {
|
||||
onboarding_step: number;
|
||||
onboarding_message: string;
|
||||
access_token: string;
|
||||
platform: 'app'|'web';
|
||||
}
|
||||
|
||||
export type UserRole =
|
||||
|
||||
@ -58,5 +58,8 @@
|
||||
<string>Roboto-Regular.ttf</string>
|
||||
<string>Roboto-SemiBold.ttf</string>
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs access to the camera to take photos and videos.</string>
|
||||
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@ -11968,9 +11968,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-svg": {
|
||||
"version": "15.12.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.0.tgz",
|
||||
"integrity": "sha512-iE25PxIJ6V0C6krReLquVw6R0QTsRTmEQc4K2Co3P6zsimU/jltcDBKYDy1h/5j9S/fqmMeXnpM+9LEWKJKI6A==",
|
||||
"version": "15.12.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz",
|
||||
"integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-select": "^5.1.0",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user