422 lines
14 KiB
TypeScript
422 lines
14 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
Alert,
|
|
Image,
|
|
ActivityIndicator,
|
|
} from 'react-native';
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import Icon from 'react-native-vector-icons/Ionicons';
|
|
import { useNavigation } from '@react-navigation/native';
|
|
import { useTheme } from '@theme';
|
|
import { FormInput, Loader } from '@components';
|
|
import { useAppDispatch, useAppSelector, RootState, updateUserData } from '@store';
|
|
import { getStyles } from './editProfile.styles';
|
|
import { updateProfile, getStaffDetails, resetEditProfileState } from './thunk';
|
|
import ImagePicker from 'react-native-image-crop-picker';
|
|
|
|
export const EditProfileScreen = () => {
|
|
const navigation = useNavigation();
|
|
const { theme: colors } = useTheme();
|
|
const styles = getStyles(colors);
|
|
const dispatch = useAppDispatch();
|
|
|
|
const userData = useAppSelector((state: RootState) => state.auth.user_data);
|
|
const { loading, loadingDetails, successMessage, error, staffDetails } = useAppSelector(
|
|
(state: RootState) => state.editProfile,
|
|
);
|
|
|
|
// Form state
|
|
const [firstname, setFirstname] = useState(userData?.firstname || '');
|
|
const [lastname, setLastname] = useState(userData?.lastname || '');
|
|
const [email, setEmail] = useState(userData?.email || '');
|
|
const [phone, setPhone] = useState(userData?.phonenumber || '');
|
|
const [emailSignature, setEmailSignature] = useState(userData?.email_signature || '');
|
|
const [password, setPassword] = useState('');
|
|
const [passwordr, setPasswordr] = useState('');
|
|
const [profileImageUri, setProfileImageUri] = useState('');
|
|
const [profileImageMime, setProfileImageMime] = useState('image/jpeg');
|
|
const [profileImageName, setProfileImageName] = useState('profile_image.jpg');
|
|
const [isImageChanged, setIsImageChanged] = useState(false);
|
|
|
|
const resolveServerUrl = async (path?: string | null) => {
|
|
if (!path) return '';
|
|
const cleanPath = path.replace(/"/g, '').trim();
|
|
if (!cleanPath) return '';
|
|
if (
|
|
cleanPath.startsWith('http') ||
|
|
cleanPath.startsWith('file://') ||
|
|
cleanPath.startsWith('content://')
|
|
) {
|
|
return cleanPath;
|
|
}
|
|
const storedBaseUrl = await AsyncStorage.getItem('base_url');
|
|
const baseUrl = storedBaseUrl || 'https://demo-convexcrm.convexsol.co';
|
|
const cleanBase = baseUrl.replace(/\/+$/, '');
|
|
const cleanRelPath = cleanPath.replace(/^\/+/, '');
|
|
return `${cleanBase}/${cleanRelPath}`;
|
|
};
|
|
|
|
useEffect(() => {
|
|
resolveServerUrl(userData?.profile_image).then((url) => {
|
|
setProfileImageUri(url);
|
|
});
|
|
}, [userData?.profile_image]);
|
|
|
|
// UI toggle state
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
|
|
// Fetch staff details on mount
|
|
// useEffect(() => {
|
|
// if (userData?.staffid) {
|
|
// dispatch(getStaffDetails(userData.staffid));
|
|
// }
|
|
// }, [dispatch, userData?.staffid]);
|
|
|
|
// Update local form state when details are loaded from the server
|
|
// useEffect(() => {
|
|
// if (staffDetails) {
|
|
// setFirstname(staffDetails.firstname || '');
|
|
// setLastname(staffDetails.lastname || '');
|
|
// setEmail(staffDetails.email || '');
|
|
// setPhone(staffDetails.phonenumber || '');
|
|
// setEmailSignature(staffDetails.email_signature || '');
|
|
// resolveServerUrl(userData?.profile_image).then((url) => {
|
|
// setProfileImageUri(url);
|
|
// });
|
|
// }
|
|
// }, [staffDetails, userData?.profile_image]);
|
|
|
|
// Reset edit profile state on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
dispatch(resetEditProfileState());
|
|
};
|
|
}, [dispatch]);
|
|
|
|
const handleCameraLaunch = () => {
|
|
ImagePicker.openCamera({
|
|
width: 400,
|
|
height: 400,
|
|
cropping: true,
|
|
mediaType: 'photo',
|
|
})
|
|
.then((image) => {
|
|
setProfileImageUri(image.path);
|
|
setProfileImageMime(image.mime || 'image/jpeg');
|
|
setProfileImageName(image.path.split('/').pop() || 'profile_image.jpg');
|
|
setIsImageChanged(true);
|
|
})
|
|
.catch((err) => {
|
|
if (err.message && !err.message.includes('cancel')) {
|
|
Alert.alert('Error', err.message || 'Failed to open camera');
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleGalleryLaunch = () => {
|
|
ImagePicker.openPicker({
|
|
width: 400,
|
|
height: 400,
|
|
cropping: true,
|
|
mediaType: 'photo',
|
|
})
|
|
.then((image) => {
|
|
setProfileImageUri(image.path);
|
|
setProfileImageMime(image.mime || 'image/jpeg');
|
|
setProfileImageName(image.path.split('/').pop() || 'profile_image.jpg');
|
|
setIsImageChanged(true);
|
|
})
|
|
.catch((err) => {
|
|
if (err.message && !err.message.includes('cancel')) {
|
|
Alert.alert('Error', err.message || 'Failed to open gallery');
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleChooseImage = () => {
|
|
Alert.alert(
|
|
'Change Profile Picture',
|
|
'Choose an option to update your photo.',
|
|
[
|
|
{
|
|
text: 'Take Photo',
|
|
onPress: handleCameraLaunch,
|
|
},
|
|
{
|
|
text: 'Choose from Gallery',
|
|
onPress: handleGalleryLaunch,
|
|
},
|
|
// {
|
|
// text: 'Remove Photo',
|
|
// style: 'destructive',
|
|
// onPress: () => setProfileImageUri(''),
|
|
// },
|
|
{ text: 'Cancel', style: 'cancel' },
|
|
],
|
|
);
|
|
};
|
|
|
|
const handleSave = () => {
|
|
if (!email.trim()) {
|
|
Alert.alert('Validation Error', 'Email address is required.');
|
|
return;
|
|
}
|
|
|
|
if (password && password !== passwordr) {
|
|
Alert.alert('Validation Error', 'New passwords do not match.');
|
|
return;
|
|
}
|
|
|
|
const staffId = userData?.staffid || '';
|
|
|
|
// Build the profile_image payload if a local file URI is selected and changed in this session
|
|
let profileImagePayload = undefined;
|
|
if (isImageChanged && profileImageUri) {
|
|
const isLocal =
|
|
!profileImageUri.startsWith('http://') &&
|
|
!profileImageUri.startsWith('https://');
|
|
|
|
const formattedUri =
|
|
isLocal &&
|
|
!profileImageUri.startsWith('file://') &&
|
|
!profileImageUri.startsWith('content://')
|
|
? `file://${profileImageUri}`
|
|
: profileImageUri;
|
|
|
|
profileImagePayload = {
|
|
uri: formattedUri,
|
|
name: profileImageName,
|
|
type: profileImageMime,
|
|
};
|
|
}
|
|
|
|
dispatch(
|
|
updateProfile({
|
|
id: staffId,
|
|
email,
|
|
firstname: firstname || undefined,
|
|
lastname: lastname || undefined,
|
|
phone: phone || undefined,
|
|
email_signature: emailSignature || undefined,
|
|
password: password || undefined,
|
|
passwordr: passwordr || undefined,
|
|
profile_image: profileImagePayload,
|
|
}),
|
|
)
|
|
.unwrap()
|
|
.then((response) => {
|
|
// Success
|
|
const updatedImage = response.user_data?.profile_image || userData?.profile_image;
|
|
dispatch(
|
|
updateUserData({
|
|
firstname,
|
|
lastname,
|
|
email,
|
|
phonenumber: phone,
|
|
email_signature: emailSignature,
|
|
profile_image: updatedImage,
|
|
}),
|
|
);
|
|
Alert.alert('Success', 'Profile updated successfully.', [
|
|
{
|
|
text: 'OK',
|
|
onPress: () => {
|
|
dispatch(resetEditProfileState());
|
|
navigation.goBack();
|
|
},
|
|
},
|
|
]);
|
|
})
|
|
.catch((err) => {
|
|
Alert.alert('Error', err || 'Failed to update profile');
|
|
});
|
|
};
|
|
|
|
const staffId = userData?.staffid || '';
|
|
const initials = `${firstname.substring(0, 1)}${lastname.substring(0, 1)}`.toUpperCase() || 'U';
|
|
const hasProfileImage = !!profileImageUri;
|
|
|
|
if (loadingDetails && !staffDetails) {
|
|
return <Loader message="Fetching profile details..." />;
|
|
}
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
style={styles.container}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
|
<ScrollView
|
|
contentContainerStyle={styles.scrollContent}
|
|
keyboardShouldPersistTaps="handled">
|
|
|
|
{/* ── Avatar Edit Section ── */}
|
|
<View style={styles.avatarSection}>
|
|
<TouchableOpacity
|
|
style={styles.avatarWrapper}
|
|
activeOpacity={0.8}
|
|
onPress={handleChooseImage}>
|
|
{hasProfileImage ? (
|
|
<Image source={{ uri: profileImageUri }} style={styles.avatarImage} />
|
|
) : (
|
|
<View style={styles.avatarCircle}>
|
|
<Text style={styles.avatarInitial}>{initials}</Text>
|
|
</View>
|
|
)}
|
|
<View style={styles.changePhotoBadge}>
|
|
<Icon name="camera" size={14} color="#FFFFFF" />
|
|
</View>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* ── Personal Info Card ── */}
|
|
<View style={styles.card}>
|
|
<Text style={styles.sectionTitle}>Basic Information</Text>
|
|
|
|
{/* Staff ID (Read-only) */}
|
|
<FormInput
|
|
label="Staff ID"
|
|
value={staffId}
|
|
onChangeText={() => {}}
|
|
editable={false}
|
|
selectTextOnFocus={false}
|
|
leftIcon={<Icon name="card-outline" size={16} color={colors.textMuted} />}
|
|
inputStyle={{ color: colors.textMuted }}
|
|
/>
|
|
|
|
{/* First & Last Name */}
|
|
<View style={styles.row}>
|
|
<View style={styles.col}>
|
|
<FormInput
|
|
label="First Name"
|
|
value={firstname}
|
|
onChangeText={setFirstname}
|
|
placeholder="First Name"
|
|
leftIcon={<Icon name="person-outline" size={16} color={colors.icon} />}
|
|
/>
|
|
</View>
|
|
<View style={styles.col}>
|
|
<FormInput
|
|
label="Last Name"
|
|
value={lastname}
|
|
onChangeText={setLastname}
|
|
placeholder="Last Name"
|
|
leftIcon={<Icon name="person-outline" size={16} color={colors.icon} />}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Email (Required) */}
|
|
<FormInput
|
|
label="Email Address"
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
required
|
|
placeholder="john.doe@company.com"
|
|
keyboardType="email-address"
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
leftIcon={<Icon name="mail-outline" size={16} color={colors.icon} />}
|
|
/>
|
|
|
|
{/* Phone */}
|
|
<FormInput
|
|
label="Phone Number"
|
|
value={phone}
|
|
onChangeText={setPhone}
|
|
placeholder="Phone Number"
|
|
keyboardType="phone-pad"
|
|
leftIcon={<Icon name="call-outline" size={16} color={colors.icon} />}
|
|
/>
|
|
</View>
|
|
|
|
{/* ── Email Signature Card ── */}
|
|
{/* <View style={styles.card}>
|
|
<Text style={styles.sectionTitle}>Email Signature</Text>
|
|
<FormInput
|
|
label="HTML Signature Block"
|
|
value={emailSignature}
|
|
onChangeText={setEmailSignature}
|
|
placeholder="<p>Best regards,<br><b>John Doe</b></p>"
|
|
multiline
|
|
inputStyle={styles.multilineInput}
|
|
/>
|
|
</View> */}
|
|
|
|
{/* ── Security Card ── */}
|
|
<View style={styles.card}>
|
|
<Text style={styles.sectionTitle}>Change Password</Text>
|
|
<Text style={styles.infoText}>
|
|
Leave password fields empty if you don't wish to change your current password.
|
|
</Text>
|
|
|
|
<FormInput
|
|
label="New Password"
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
placeholder="••••••••"
|
|
secureTextEntry={!showPassword}
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
leftIcon={<Icon name="lock-closed-outline" size={16} color={colors.icon} />}
|
|
rightIcon={
|
|
<TouchableOpacity
|
|
onPress={() => setShowPassword(!showPassword)}
|
|
activeOpacity={0.7}>
|
|
<Icon
|
|
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
|
|
size={16}
|
|
color={colors.textMuted}
|
|
/>
|
|
</TouchableOpacity>
|
|
}
|
|
/>
|
|
|
|
<FormInput
|
|
label="Repeat New Password"
|
|
value={passwordr}
|
|
onChangeText={setPasswordr}
|
|
placeholder="••••••••"
|
|
secureTextEntry={!showConfirmPassword}
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
leftIcon={<Icon name="lock-closed-outline" size={16} color={colors.icon} />}
|
|
rightIcon={
|
|
<TouchableOpacity
|
|
onPress={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
activeOpacity={0.7}>
|
|
<Icon
|
|
name={showConfirmPassword ? 'eye-off-outline' : 'eye-outline'}
|
|
size={16}
|
|
color={colors.textMuted}
|
|
/>
|
|
</TouchableOpacity>
|
|
}
|
|
/>
|
|
</View>
|
|
|
|
{/* ── Save Button ── */}
|
|
<TouchableOpacity
|
|
style={[styles.saveButton, loading && { opacity: 0.7 }]}
|
|
activeOpacity={0.8}
|
|
onPress={handleSave}
|
|
disabled={loading}>
|
|
{loading ? (
|
|
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
) : (
|
|
<>
|
|
<Icon name="checkmark-circle-outline" size={20} color="#FFFFFF" />
|
|
<Text style={styles.saveButtonText}>Save Changes</Text>
|
|
</>
|
|
)}
|
|
</TouchableOpacity>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
);
|
|
};
|