NeoScan_Radiologist/app/modules/Settings/screens/SettingsScreen.tsx

945 lines
26 KiB
TypeScript

/*
* File: SettingsScreen.tsx
* Description: Main settings screen with profile management and app preferences
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
Alert,
RefreshControl,
Image,
TouchableOpacity,
ActivityIndicator,
ActionSheetIOS,
Platform,
PermissionsAndroid,
} from 'react-native';
import { theme } from '../../../theme/theme';
import {
SettingsSection,
SettingsSectionData,
SettingsItem
} from '../../../shared/types';
import { SettingsHeader } from '../components/SettingsHeader';
import { SettingsSectionComponent } from '../components/SettingsSectionComponent';
import { ProfileCard } from '../components/ProfileCard';
import { CustomModal } from '../../../shared/components';
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
import { logoutUser } from '../../Auth/redux/authActions';
import { updateUserProfile } from '../../Auth/redux/authSlice';
import {
selectUser,
selectUserDisplayName,
selectUserEmail,
selectUserFirstName,
selectUserLastName,
selectUserProfilePhoto,
selectDashboardSettings
} from '../../Auth/redux/authSelectors';
import { API_CONFIG } from '../../../shared/utils';
import { authAPI } from '../../Auth/services/authAPI';
import {
launchImageLibrary,
launchCamera,
ImagePickerResponse,
MediaType
} from 'react-native-image-picker';
import Icon from 'react-native-vector-icons/Feather';
/**
* SettingsScreenProps Interface
*
* Purpose: Defines the props required by the SettingsScreen component
*
* Props:
* - navigation: React Navigation object for screen navigation
*/
interface SettingsScreenProps {
navigation: any;
}
/**
* SettingsScreen Component
*
* Purpose: Main settings screen for user profile management and app preferences
*
* Features:
* 1. User profile overview and quick access
* 2. Comprehensive settings sections
* 3. Navigation to detailed settings screens
* 4. Pull-to-refresh functionality
* 5. Mock data generation for demonstration
*
* Settings Sections:
* - Profile: Personal and professional information
* - Notifications: Alert and notification preferences
* - Clinical: Clinical workflow preferences
* - Privacy: Security and privacy settings
* - Accessibility: Accessibility features
* - About: App information and help
* - Logout: Sign out functionality
*/
export const SettingsScreen: React.FC<SettingsScreenProps> = ({
navigation,
}) => {
// ============================================================================
// STATE MANAGEMENT
// ============================================================================
// Settings sections state
const [settingsSections, setSettingsSections] = useState<SettingsSectionData[]>([]);
// UI state
const [refreshing, setRefreshing] = useState(false);
// Profile photo state
const [uploadingPhoto, setUploadingPhoto] = useState(false);
const [tempProfilePhoto, setTempProfilePhoto] = useState<string | null>(null);
// Upload response interface
interface UploadPhotoResponse {
success: boolean;
message?: string;
data?: {
profile_photo_url: string;
};
}
// Modal state
const [modalVisible, setModalVisible] = useState(false);
const [modalConfig, setModalConfig] = useState({
title: '',
message: '',
type: 'info' as 'success' | 'error' | 'warning' | 'info' | 'confirm',
onConfirm: () => {},
showCancel: false,
icon: '',
});
// Redux dispatch and selectors
const dispatch = useAppDispatch();
// User data from Redux
const user = useAppSelector(selectUser);
const userDisplayName = useAppSelector(selectUserDisplayName);
const userEmail = useAppSelector(selectUserEmail);
const userFirstName = useAppSelector(selectUserFirstName);
const userLastName = useAppSelector(selectUserLastName);
const userProfilePhoto = useAppSelector(selectUserProfilePhoto);
const dashboardSettings = useAppSelector(selectDashboardSettings);
// ============================================================================
// SETTINGS SECTIONS GENERATION
// ============================================================================
/**
* generateSettingsSections Function
*
* Purpose: Generate settings sections with items for the settings screen
*
* Returns: Array of SettingsSectionData with navigation and action items
*/
const generateSettingsSections = (): SettingsSectionData[] => [
{
id: 'PROFILE',
title: 'Profile & Account',
items: [
{
id: 'edit-profile',
title: 'Edit Profile',
subtitle: 'Update personal and professional information',
icon: 'user',
type: 'NAVIGATION',
onPress: () => handleNavigation('PROFILE'),
},
{
id: 'change-password',
title: 'Change Password',
subtitle: 'Update your account password',
icon: 'lock',
type: 'NAVIGATION',
onPress: () => handleNavigation('CHANGE_PASSWORD'),
},
],
},
{
id: 'ABOUT',
title: 'About & Support',
items: [
{
id: 'app-info',
title: 'App Information',
subtitle: 'Version, build number, and details',
icon: 'smartphone',
type: 'NAVIGATION',
onPress: () => handleNavigation('APP_INFO'),
},
{
id: 'help-support',
title: 'Help & Support',
subtitle: 'Contact support and view documentation',
icon: 'phone',
type: 'NAVIGATION',
onPress: () => handleNavigation('HELP'),
},
],
},
{
id: 'LOGOUT',
title: 'Account',
items: [
{
id: 'logout',
title: 'Sign Out',
subtitle: 'Sign out of your account',
icon: 'log-out',
type: 'ACTION',
onPress: handleLogout,
},
],
},
];
// ============================================================================
// EFFECTS
// ============================================================================
/**
* useEffect for initial settings sections generation
*
* Purpose: Generate settings sections when component mounts or user data changes
*/
useEffect(() => {
setSettingsSections(generateSettingsSections());
}, [user, dashboardSettings]);
// ============================================================================
// PERMISSION HANDLERS
// ============================================================================
/**
* requestCameraPermission Function
*
* 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 profile photos.',
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; // iOS permissions are handled via Info.plist
};
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* handleProfilePhotoUpdate Function
*
* Purpose: Show action sheet with camera and gallery options for profile photo update
*
* Flow:
* 1. Show action sheet with camera and gallery options
* 2. Handle user selection
* 3. Launch appropriate image picker
*/
const handleProfilePhotoUpdate = () => {
if (Platform.OS === 'ios') {
ActionSheetIOS.showActionSheetWithOptions(
{
options: ['Cancel', 'Take Photo', 'Choose from Gallery'],
cancelButtonIndex: 0,
userInterfaceStyle: 'light',
},
(buttonIndex) => {
if (buttonIndex === 1) {
handleCameraCapture();
} else if (buttonIndex === 2) {
handleGallerySelection();
}
}
);
} else {
// For Android, show custom action sheet or Alert
Alert.alert(
'Update Profile Photo',
'Choose an option',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Take Photo', onPress: handleCameraCapture },
{ text: 'Choose from Gallery', onPress: handleGallerySelection },
]
);
}
};
/**
* handleCameraCapture Function
*
* Purpose: Launch camera to capture new profile photo
*
* Flow:
* 1. Check camera permissions
* 2. Launch camera with callback
* 3. Validate captured image
* 4. Upload to server
* 5. Update local state
*/
const handleCameraCapture = async () => {
try {
// Check camera permission first
const hasPermission = await requestCameraPermission();
if (!hasPermission) {
setModalConfig({
title: 'Permission Required',
message: 'Camera permission is required to capture profile photos.',
type: 'error',
onConfirm: () => {},
showCancel: false,
icon: 'camera',
});
setModalVisible(true);
return;
}
// Launch camera with callback
const options = {
mediaType: 'photo' as MediaType,
quality: 0.8 as const,
maxWidth: 800,
maxHeight: 800,
saveToPhotos: false,
includeBase64: false,
};
launchCamera(options, (response: ImagePickerResponse) => {
try {
// Handle user cancellation
if (response.didCancel) {
return;
}
// Handle errors
if (response.errorMessage) {
throw new Error(response.errorMessage);
}
// Validate response and assets
if (!response.assets || response.assets.length === 0) {
throw new Error('No image captured');
}
const asset = response.assets[0];
if (!asset.uri) {
throw new Error('Invalid image data');
}
// Validate file size (max 5MB)
if (asset.fileSize && asset.fileSize > 5 * 1024 * 1024) {
throw new Error('Image size must be less than 5MB');
}
// Set temporary photo for preview
setTempProfilePhoto(asset.uri);
// Upload the captured photo
uploadProfilePhoto(asset.uri);
} catch (error) {
console.error('Camera capture processing error:', error);
setModalConfig({
title: 'Camera Error',
message: error instanceof Error ? error.message : 'Failed to capture photo',
type: 'error',
onConfirm: () => {},
showCancel: false,
icon: 'alert-circle',
});
setModalVisible(true);
}
});
} catch (error) {
console.error('Camera launch error:', error);
setModalConfig({
title: 'Error',
message: 'Failed to launch camera. Please try again.',
type: 'error',
onConfirm: () => {},
showCancel: false,
icon: 'alert-circle',
});
setModalVisible(true);
}
};
/**
* handleGallerySelection Function
*
* Purpose: Launch gallery to select existing profile photo
*
* Flow:
* 1. Launch image picker with callback
* 2. Validate selected image
* 3. Upload to server
* 4. Update local state
*/
const handleGallerySelection = () => {
try {
// Launch image picker with callback
const options = {
mediaType: 'photo' as MediaType,
quality: 0.8 as const,
maxWidth: 800,
maxHeight: 800,
includeBase64: false,
};
launchImageLibrary(options, (response: ImagePickerResponse) => {
try {
// Handle user cancellation
if (response.didCancel) {
return;
}
// Handle errors
if (response.errorMessage) {
throw new Error(response.errorMessage);
}
// Validate response and assets
if (!response.assets || response.assets.length === 0) {
throw new Error('No image selected');
}
const asset = response.assets[0];
if (!asset.uri) {
throw new Error('Invalid image data');
}
// Validate file size (max 5MB)
if (asset.fileSize && asset.fileSize > 5 * 1024 * 1024) {
throw new Error('Image size must be less than 5MB');
}
// Set temporary photo for preview
setTempProfilePhoto(asset.uri);
// Upload the selected photo
uploadProfilePhoto(asset.uri);
} catch (error) {
console.error('Gallery selection processing error:', error);
setModalConfig({
title: 'Gallery Error',
message: error instanceof Error ? error.message : 'Failed to select photo',
type: 'error',
onConfirm: () => {},
showCancel: false,
icon: 'alert-circle',
});
setModalVisible(true);
}
});
} catch (error) {
console.error('Gallery launch error:', error);
setModalConfig({
title: 'Error',
message: 'Failed to open gallery. Please try again.',
type: 'error',
onConfirm: () => {},
showCancel: false,
icon: 'alert-circle',
});
setModalVisible(true);
}
};
/**
* uploadProfilePhoto Function
*
* Purpose: Upload selected profile photo to server
*
* @param imageUri - URI of the selected image
*/
const uploadProfilePhoto = async (imageUri: string) => {
try {
setUploadingPhoto(true);
// Create form data
const formData = new FormData();
formData.append('profile_photo', {
uri: imageUri,
type: 'image/jpeg',
name: 'profile_photo.jpg',
} as any);
// Get user token from Redux
const token = user?.access_token;
if (!token) {
throw new Error('Authentication token not found');
}
// Upload using authAPI
const response = await authAPI.uploadProfilePhoto(formData, token);
// Type the response properly
const responseData = response.data as UploadPhotoResponse;
if (responseData.success) {
// Update local state with new photo
setTempProfilePhoto(null);
// Update Redux state with new profile photo URL
if (responseData.data?.profile_photo_url) {
dispatch(updateUserProfile({
self_url: responseData.data.profile_photo_url
}));
console.log('Redux state updated successfully');
}
// Show success message
setModalConfig({
title: 'Success',
message: responseData.message || 'Profile photo updated successfully!',
type: 'success',
icon: 'check-circle',
onConfirm: () => {
// Optional: Refresh if needed, but Redux update should be enough
// handleRefresh();
},
showCancel: false,
});
setModalVisible(true);
} else {
throw new Error(responseData.message || 'Upload failed');
}
} catch (error) {
console.error('Error uploading photo:', error);
setModalConfig({
title: 'Upload Failed',
message: error instanceof Error ? error.message : 'Failed to upload profile photo',
type: 'error',
icon: 'alert-circle',
onConfirm: () => {},
showCancel: false,
});
setModalVisible(true);
} finally {
setUploadingPhoto(false);
}
};
/**
* handleRefresh Function
*
* Purpose: Handle pull-to-refresh functionality to update settings data
*
* Flow:
* 1. Set refreshing state to true (show loading indicator)
* 2. Simulate API call with delay
* 3. Regenerate settings sections with current user data
* 4. Set refreshing state to false (hide loading indicator)
*/
const handleRefresh = async () => {
setRefreshing(true);
// Simulate API call with 1-second delay
await new Promise<void>(resolve => setTimeout(() => resolve(), 1000));
// Regenerate settings sections with current user data
setSettingsSections(generateSettingsSections());
setRefreshing(false);
};
/**
* handleNavigation Function
*
* Purpose: Handle navigation to different settings screens
*
* @param screen - Screen to navigate to
*/
const handleNavigation = (screen: string) => {
switch (screen) {
case 'APP_INFO':
navigation.navigate('AppInfoScreen');
break;
case 'PROFILE':
navigation.navigate('EditProfileScreen');
break;
case 'CHANGE_PASSWORD':
navigation.navigate('ChangePasswordScreen');
break;
case 'HELP':
// TODO: Implement help and support
setModalConfig({
title: 'Help & Support',
message: 'Help and support functionality coming soon!',
type: 'info',
onConfirm: () => {},
showCancel: false,
icon: 'info',
});
setModalVisible(true);
break;
default:
console.log('Navigate to:', screen);
setModalConfig({
title: 'Navigation',
message: `Navigate to ${screen} screen`,
type: 'info',
onConfirm: () => {},
showCancel: false,
icon: 'info',
});
setModalVisible(true);
}
};
/**
* handleToggleSetting Function
*
* Purpose: Handle toggle settings changes
*
* @param setting - Setting to toggle
*/
const handleToggleSetting = (setting: string) => {
// TODO: Implement setting toggle logic
console.log('Toggle setting:', setting);
setModalConfig({
title: 'Setting Toggle',
message: `Toggle ${setting} setting`,
type: 'info',
icon: 'info',
onConfirm: () => {},
showCancel: false,
});
setModalVisible(true);
};
/**
* handleLogout Function
*
* Purpose: Handle user logout with Redux integration
*
* Flow:
* 1. Show confirmation dialog
* 2. Dispatch logout action to Redux
* 3. Clear authentication state
* 4. Show success message
* 5. Automatically navigate to login screen via Redux state change
*/
const handleLogout = () => {
setModalConfig({
title: 'Sign Out',
message: 'Are you sure you want to sign out?',
type: 'confirm',
icon: 'log-out',
onConfirm: async () => {
try {
// Dispatch logout thunk to Redux
await dispatch(logoutUser());
// Log the logout action
console.log('User logged out successfully');
} catch (error) {
console.error('Logout error:', error);
setModalConfig({
title: 'Error',
message: 'Failed to sign out. Please try again.',
type: 'error',
icon: 'info',
onConfirm: () => {},
showCancel: false,
});
setModalVisible(true);
}
},
showCancel: true,
});
setModalVisible(true);
};
// ============================================================================
// MAIN RENDER
// ============================================================================
return (
<View style={styles.container}>
{/* Settings header with title */}
<SettingsHeader title="Settings" />
{/* Scrollable settings content */}
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
colors={[theme.colors.primary]}
tintColor={theme.colors.primary}
/>
}
showsVerticalScrollIndicator={false}
>
{/* Profile card section */}
{user && (
<View style={styles.profileCard}>
<View style={styles.profileHeader}>
<TouchableOpacity
style={styles.profileImageContainer}
onPress={handleProfilePhotoUpdate}
disabled={uploadingPhoto}
>
{tempProfilePhoto ? (
<Image
source={{ uri: tempProfilePhoto }}
style={styles.profileImage}
resizeMode="cover"
/>
) : user.self_url ? (
<Image
source={{ uri: API_CONFIG.BASE_URL + '/api/auth' + user.self_url }}
style={styles.profileImage}
resizeMode="cover"
/>
) : (
<View style={styles.fallbackAvatar}>
<Text style={styles.fallbackText}>
{user.first_name.charAt(0)}{user.last_name.charAt(0)}
</Text>
</View>
)}
{/* Edit icon overlay */}
<View style={styles.editIconOverlay}>
<Icon name="edit-3" size={16} color={theme.colors.background} />
</View>
{/* Loading indicator */}
{uploadingPhoto && (
<View style={styles.uploadingOverlay}>
<ActivityIndicator
size="small"
color={theme.colors.primary}
/>
</View>
)}
</TouchableOpacity>
<View style={styles.profileInfo}>
<Text style={styles.profileName}>
{user.display_name || `${user.first_name} ${user.last_name}`}
</Text>
<Text style={styles.profileEmail}>{user.email}</Text>
<Text style={styles.profileRole}>Radiologist</Text>
</View>
</View>
</View>
)}
{/* Settings sections */}
{settingsSections.map((section, index) =>
React.createElement(SettingsSectionComponent, {
key: `${section.id}-${index}`,
section: section
})
)}
{/* Bottom spacing for tab bar */}
<View style={styles.bottomSpacing} />
</ScrollView>
{/* Custom Modal */}
<CustomModal
visible={modalVisible}
title={modalConfig.title}
message={modalConfig.message}
type={modalConfig.type}
onConfirm={modalConfig.onConfirm}
showCancel={modalConfig.showCancel}
icon={modalConfig.icon}
confirmText={modalConfig.type === 'confirm' ? 'Sign Out' : 'OK'}
cancelText="Cancel"
onClose={() => setModalVisible(false)}
/>
</View>
);
};
// ============================================================================
// STYLES SECTION
// ============================================================================
const styles = StyleSheet.create({
// Main container for the settings screen
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Loading container for initial data loading
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: theme.colors.background,
},
// Loading text styling
loadingText: {
fontSize: theme.typography.fontSize.bodyLarge,
color: theme.colors.textSecondary,
fontFamily: theme.typography.fontFamily.medium,
},
// Scroll view styling
scrollView: {
flex: 1,
},
// Scroll content styling
scrollContent: {
paddingHorizontal: theme.spacing.md,
},
// Bottom spacing for tab bar
bottomSpacing: {
height: theme.spacing.xl,
},
// Profile card styles
profileCard: {
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md,
marginBottom: theme.spacing.md,
...theme.shadows.primary,
},
profileHeader: {
flexDirection: 'row',
alignItems: 'center',
},
profileImageContainer: {
marginRight: theme.spacing.md,
},
profileImage: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor:theme.colors.primary,
},
fallbackAvatar: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: theme.colors.primary,
justifyContent: 'center',
alignItems: 'center',
},
fallbackText: {
color: theme.colors.background,
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.bold,
},
profileInfo: {
flex: 1,
},
profileName: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
profileEmail: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
},
profileRole: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.primary,
},
// Edit icon overlay for profile photo update
editIconOverlay: {
position: 'absolute',
bottom: 0,
right: 0,
backgroundColor: theme.colors.primary,
borderRadius: 12,
width: 24,
height: 24,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: theme.colors.background,
},
// Uploading overlay with loading indicator
uploadingOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.5)',
borderRadius: 30,
},
});
/*
* End of File: SettingsScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/