NeoScan_Physician/app/modules/Settings/screens/EditProfileScreen.tsx
2025-08-20 20:42:33 +05:30

638 lines
18 KiB
TypeScript

/*
* File: EditProfileScreen.tsx
* Description: Edit profile screen for updating user information
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TextInput,
TouchableOpacity,
Alert,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { theme } from '../../../theme/theme';
import { SettingsHeader } from '../components/SettingsHeader';
import { useAppSelector, useAppDispatch } from '../../../store/hooks';
import {
selectUserFirstName,
selectUserLastName,
selectUserDisplayName,
selectUserEmail,
selectUser,
} from '../../Auth/redux/authSelectors';
import { updateUserProfileAsync } from '../../Auth/redux/authActions';
/**
* EditProfileScreenProps Interface
*
* Purpose: Defines the props required by the EditProfileScreen component
*
* Props:
* - navigation: React Navigation object for screen navigation
*/
interface EditProfileScreenProps {
navigation: any;
}
/**
* FormData Interface
*
* Purpose: Defines the structure of the profile form data
*/
interface FormData {
firstName: string;
lastName: string;
displayName: string;
}
/**
* FormErrors Interface
*
* Purpose: Defines the structure of form validation errors
*/
interface FormErrors {
firstName?: string;
lastName?: string;
displayName?: string;
}
/**
* EditProfileScreen Component
*
* Purpose: Allows users to edit their profile information including first name,
* last name, and display name with proper validation
*
* Features:
* 1. Pre-populated form with current user data
* 2. Real-time validation
* 3. Form submission with error handling
* 4. Clean and intuitive user interface
*/
export const EditProfileScreen: React.FC<EditProfileScreenProps> = ({
navigation,
}) => {
// ============================================================================
// REDUX STATE
// ============================================================================
const dispatch = useAppDispatch();
const user = useAppSelector(selectUser);
const currentFirstName = useAppSelector(selectUserFirstName);
const currentLastName = useAppSelector(selectUserLastName);
const currentDisplayName = useAppSelector(selectUserDisplayName);
const currentEmail = useAppSelector(selectUserEmail);
// ============================================================================
// LOCAL STATE
// ============================================================================
const [formData, setFormData] = useState<FormData>({
firstName: '',
lastName: '',
displayName: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
// ============================================================================
// EFFECTS
// ============================================================================
/**
* Initialize form data with current user information
*/
useEffect(() => {
setFormData({
firstName: currentFirstName || '',
lastName: currentLastName || '',
displayName: currentDisplayName || '',
});
}, [currentFirstName, currentLastName, currentDisplayName]);
/**
* Check if form has unsaved changes
*/
useEffect(() => {
const hasUnsavedChanges =
formData.firstName !== currentFirstName ||
formData.lastName !== currentLastName ||
formData.displayName !== currentDisplayName;
setHasChanges(hasUnsavedChanges);
}, [formData, currentFirstName, currentLastName, currentDisplayName]);
// ============================================================================
// VALIDATION FUNCTIONS
// ============================================================================
/**
* validateField Function
*
* Purpose: Validate individual form fields
*
* @param field - Field name to validate
* @param value - Field value to validate
* @returns Validation error message or undefined
*/
const validateField = (field: keyof FormData, value: string): string | undefined => {
switch (field) {
case 'firstName':
if (!value.trim()) {
return 'First name is required';
}
if (value.trim().length < 2) {
return 'First name must be at least 2 characters';
}
if (value.trim().length > 50) {
return 'First name must be less than 50 characters';
}
if (!/^[a-zA-Z\s'-]+$/.test(value.trim())) {
return 'First name can only contain letters, spaces, hyphens, and apostrophes';
}
break;
case 'lastName':
if (!value.trim()) {
return 'Last name is required';
}
if (value.trim().length < 1) {
return 'Last name must be at least 1 character';
}
if (value.trim().length > 50) {
return 'Last name must be less than 50 characters';
}
if (!/^[a-zA-Z\s'-]+$/.test(value.trim())) {
return 'Last name can only contain letters, spaces, hyphens, and apostrophes';
}
break;
case 'displayName':
if (!value.trim()) {
return 'Display name is required';
}
if (value.trim().length < 2) {
return 'Display name must be at least 2 characters';
}
if (value.trim().length > 30) {
return 'Display name must be less than 30 characters';
}
break;
}
return undefined;
};
/**
* validateForm Function
*
* Purpose: Validate entire form and return validation errors
*
* @returns Object containing validation errors
*/
const validateForm = (): FormErrors => {
const newErrors: FormErrors = {};
Object.keys(formData).forEach((field) => {
const key = field as keyof FormData;
const error = validateField(key, formData[key]);
if (error) {
newErrors[key] = error;
}
});
return newErrors;
};
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* handleInputChange Function
*
* Purpose: Handle input field changes and clear field-specific errors
*
* @param field - Field name that changed
* @param value - New field value
*/
const handleInputChange = (field: keyof FormData, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Clear field-specific error when user starts typing
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
};
/**
* handleInputBlur Function
*
* Purpose: Validate field when user leaves the input
*
* @param field - Field name to validate
*/
const handleInputBlur = (field: keyof FormData) => {
const error = validateField(field, formData[field]);
setErrors(prev => ({ ...prev, [field]: error }));
};
/**
* handleSubmit Function
*
* Purpose: Handle form submission with validation and API call
*/
const handleSubmit = async () => {
// Validate form
const validationErrors = validateForm();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
setIsSubmitting(true);
try {
// Dispatch update action
await dispatch(updateUserProfileAsync({
first_name: formData.firstName.trim(),
last_name: formData.lastName.trim(),
})).unwrap();
// Navigate back after successful profile update
navigation.goBack();
} catch (error: any) {
// Handle error - toast notification is already shown in the thunk
console.error('Profile update error:', error);
} finally {
setIsSubmitting(false);
}
};
/**
* handleCancel Function
*
* Purpose: Handle cancel action with unsaved changes warning
*/
const handleCancel = () => {
if (hasChanges) {
Alert.alert(
'Unsaved Changes',
'You have unsaved changes. Are you sure you want to leave?',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Leave',
style: 'destructive',
onPress: () => navigation.goBack(),
},
]
);
} else {
navigation.goBack();
}
};
// ============================================================================
// MAIN RENDER
// ============================================================================
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
{/* Header with back button */}
<SettingsHeader
title="Edit Profile"
showBackButton={true}
onBackPress={handleCancel}
/>
{/* Scrollable form content */}
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{/* Current email display (read-only) */}
<View style={styles.infoSection}>
<Text style={styles.sectionTitle}>Account Information</Text>
<View style={styles.readOnlyField}>
<Text style={styles.readOnlyLabel}>Email Address</Text>
<View style={styles.emailContainer}>
<Text style={styles.readOnlyValue}>{currentEmail}</Text>
<View style={styles.lockIcon}>
<Icon name="lock" size={16} color={theme.colors.textMuted} />
</View>
</View>
<Text style={styles.readOnlyHint}>Email address cannot be changed</Text>
</View>
</View>
{/* Profile form section */}
<View style={styles.infoSection}>
<Text style={styles.sectionTitle}>Personal Information</Text>
{/* First Name Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>First Name *</Text>
<TextInput
style={[
styles.textInput,
errors.firstName ? styles.inputError : null,
]}
value={formData.firstName}
onChangeText={(value) => handleInputChange('firstName', value)}
onBlur={() => handleInputBlur('firstName')}
placeholder="Enter your first name"
placeholderTextColor={theme.colors.textMuted}
autoCapitalize="words"
autoCorrect={false}
maxLength={50}
/>
{errors.firstName && (
<Text style={styles.errorText}>{errors.firstName}</Text>
)}
</View>
{/* Last Name Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Last Name *</Text>
<TextInput
style={[
styles.textInput,
errors.lastName ? styles.inputError : null,
]}
value={formData.lastName}
onChangeText={(value) => handleInputChange('lastName', value)}
onBlur={() => handleInputBlur('lastName')}
placeholder="Enter your last name"
placeholderTextColor={theme.colors.textMuted}
autoCapitalize="words"
autoCorrect={false}
maxLength={50}
/>
{errors.lastName && (
<Text style={styles.errorText}>{errors.lastName}</Text>
)}
</View>
{/* Display Name Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Display Name *</Text>
<TextInput
style={[
styles.textInput,
errors.displayName ? styles.inputError : null,
]}
value={formData.displayName}
onChangeText={(value) => handleInputChange('displayName', value)}
onBlur={() => handleInputBlur('displayName')}
placeholder="Enter your display name"
placeholderTextColor={theme.colors.textMuted}
autoCapitalize="words"
autoCorrect={false}
maxLength={30}
/>
{errors.displayName && (
<Text style={styles.errorText}>{errors.displayName}</Text>
)}
</View>
</View>
{/* Action buttons */}
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[
styles.submitButton,
(!hasChanges || isSubmitting) && styles.submitButtonDisabled,
]}
onPress={handleSubmit}
disabled={!hasChanges || isSubmitting}
activeOpacity={0.7}
>
<Text style={[
styles.submitButtonText,
(!hasChanges || isSubmitting) && styles.submitButtonTextDisabled,
]}>
{isSubmitting ? 'Updating...' : 'Update Profile'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.cancelButton}
onPress={handleCancel}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
</View>
{/* Bottom spacing for tab bar */}
<View style={styles.bottomSpacing} />
</ScrollView>
</KeyboardAvoidingView>
);
};
// ============================================================================
// STYLES SECTION
// ============================================================================
const styles = StyleSheet.create({
// Main container
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Scroll view styling
scrollView: {
flex: 1,
},
// Scroll content styling
scrollContent: {
paddingHorizontal: theme.spacing.md,
},
// Bottom spacing for tab bar
bottomSpacing: {
height: theme.spacing.xl,
},
// Information sections
infoSection: {
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md,
marginBottom: theme.spacing.md,
...theme.shadows.primary,
},
sectionTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.md,
},
// Read-only field styling
readOnlyField: {
paddingVertical: theme.spacing.sm,
borderBottomColor: theme.colors.border,
borderBottomWidth: 1,
},
readOnlyLabel: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textMuted,
marginBottom: theme.spacing.xs,
},
emailContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: theme.spacing.xs,
},
readOnlyValue: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textPrimary,
flex: 1,
},
lockIcon: {
padding: theme.spacing.xs,
backgroundColor: theme.colors.backgroundAlt,
borderRadius: theme.borderRadius.small,
},
readOnlyHint: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textMuted,
fontStyle: 'italic',
},
// Input container styling
inputContainer: {
marginBottom: theme.spacing.sm,
backgroundColor: theme.colors.background,
padding: theme.spacing.sm,
borderRadius: theme.borderRadius.small,
},
inputLabel: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
fontWeight: '600',
},
textInput: {
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.md,
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textPrimary,
backgroundColor: theme.colors.background,
minHeight: 48,
},
inputError: {
borderColor: theme.colors.error,
backgroundColor: theme.colors.error + '10',
},
errorText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.error,
marginTop: theme.spacing.sm,
marginLeft: theme.spacing.xs,
paddingHorizontal: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
backgroundColor: theme.colors.error + '10',
borderRadius: theme.borderRadius.small,
alignSelf: 'flex-start',
},
// Button container
buttonContainer: {
marginTop: theme.spacing.lg,
marginBottom: theme.spacing.md,
},
submitButton: {
backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
marginBottom: theme.spacing.md,
...theme.shadows.primary,
},
submitButtonDisabled: {
backgroundColor: theme.colors.border,
shadowColor: 'transparent',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0,
shadowRadius: 0,
elevation: 0,
},
submitButtonText: {
color: theme.colors.background,
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.bold,
},
submitButtonTextDisabled: {
color: theme.colors.textMuted,
},
cancelButton: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
},
cancelButtonText: {
color: theme.colors.textSecondary,
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.medium,
},
});
/*
* End of File: EditProfileScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/