new onboard flow for upload id document added

This commit is contained in:
yashwin-foxy 2025-08-06 19:09:06 +05:30
parent 7a9e610b7f
commit 848bd3ea93
18 changed files with 1938 additions and 279 deletions

View File

@ -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"

View File

@ -1,3 +1,3 @@
<resources>
<string name="app_name">NeoScan_Physician</string>
<string name="app_name">NeoScanPhysician</string>
</resources>

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

View File

@ -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 */}

View File

@ -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

View File

@ -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,

View File

@ -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
};

View 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.
*/

View File

@ -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',

View 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';

View File

@ -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,
},
});

View File

@ -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}>

View File

@ -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

View 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.
*/

View File

@ -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

View File

@ -43,6 +43,7 @@ export interface User {
onboarding_step: number;
onboarding_message: string;
access_token: string;
platform: 'app'|'web';
}
export type UserRole =

View File

@ -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
View File

@ -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",