profile update works

This commit is contained in:
2025-07-03 23:47:22 +05:00
parent 77e3ca0f18
commit e2b696655d
5 changed files with 542 additions and 5 deletions

View File

@@ -0,0 +1,470 @@
import React, { useState, useRef } from 'react';
import {
View,
Text,
Modal,
StyleSheet,
SafeAreaView,
TouchableOpacity,
Alert,
ActivityIndicator,
ScrollView,
TouchableWithoutFeedback,
Keyboard,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import Input from './Input';
import Button from './Button';
import { COLORS } from '../constants/colors';
const PASSPORT_SERIES = [
'I-AS', 'I-MR', 'II-MR', 'I-AH', 'II-AH',
'I-LB', 'II-LB', 'I-BN', 'II-BN', 'I-DZ', 'II-DZ'
];
const EditProfileModal = ({
visible,
onClose,
onSave,
initialData = {},
isLoading = false
}) => {
const [formData, setFormData] = useState({
name: initialData.name || '',
phone: initialData.phone ? initialData.phone.toString() : '',
password: '',
passport_serie: initialData.passport_serie || '',
passport_id: initialData.passport_id ? initialData.passport_id.toString() : '',
});
const [errors, setErrors] = useState({});
const [showPassportPicker, setShowPassportPicker] = useState(false);
const phoneInputRef = useRef(null);
const passwordInputRef = useRef(null);
const passportIdInputRef = useRef(null);
const validateForm = () => {
const newErrors = {};
// Name validation (required, max 255 characters)
if (!formData.name.trim()) {
newErrors.name = 'At gerek';
} else if (formData.name.length > 255) {
newErrors.name = 'At 255 harpdan köp bolmaly däl';
}
// Phone validation (required, should be number)
if (!formData.phone.trim()) {
newErrors.phone = 'Telefon belgisi gerek';
} else if (!/^\d+$/.test(formData.phone)) {
newErrors.phone = 'Telefon belgisi diňe sanlardan durmalı';
} else if (formData.phone.length < 8) {
newErrors.phone = 'Telefon belgisi azyndan 8 san bolmaly';
}
// Password validation (optional, but if provided should be strong)
if (formData.password && formData.password.length < 6) {
newErrors.password = 'Parol azyndan 6 harp bolmaly';
}
// Passport ID validation (optional, but if provided should be number)
if (formData.passport_id && !/^\d+$/.test(formData.passport_id)) {
newErrors.passport_id = 'Passport ID diňe sanlardan durmalı';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSave = () => {
if (!validateForm()) {
return;
}
// Prepare data for API call
const updateData = {
name: formData.name.trim(),
phone: parseInt(formData.phone),
};
// Only include optional fields if they're provided
if (formData.password) {
updateData.password = formData.password;
}
if (formData.passport_serie) {
updateData.passport_serie = formData.passport_serie;
}
if (formData.passport_id) {
updateData.passport_id = parseInt(formData.passport_id);
}
onSave(updateData);
};
const handleClose = () => {
// Reset form data and errors
setFormData({
name: initialData.name || '',
phone: initialData.phone ? initialData.phone.toString() : '',
password: '',
passport_serie: initialData.passport_serie || '',
passport_id: initialData.passport_id ? initialData.passport_id.toString() : '',
});
setErrors({});
onClose();
};
const updateFormData = (field, value) => {
setFormData(prev => ({
...prev,
[field]: value
}));
// Clear error for this field if it exists
if (errors[field]) {
setErrors(prev => ({
...prev,
[field]: null
}));
}
};
const renderPassportSeriesPicker = () => {
if (!showPassportPicker) return null;
return (
<Modal
visible={showPassportPicker}
transparent={true}
animationType="slide"
onRequestClose={() => setShowPassportPicker(false)}
>
<TouchableWithoutFeedback onPress={() => setShowPassportPicker(false)}>
<View style={styles.modalOverlay}>
<View style={styles.pickerContainer}>
<View style={styles.pickerHeader}>
<Text style={styles.pickerTitle}>Passport seriýasy</Text>
<TouchableOpacity onPress={() => setShowPassportPicker(false)}>
<Text style={styles.pickerDoneText}>Boldy</Text>
</TouchableOpacity>
</View>
<ScrollView style={styles.pickerList}>
<TouchableOpacity
style={styles.pickerItem}
onPress={() => {
updateFormData('passport_serie', '');
setShowPassportPicker(false);
}}
>
<Text style={[styles.pickerItemText, styles.placeholderText]}>
Saýlaň
</Text>
</TouchableOpacity>
{PASSPORT_SERIES.map((series) => (
<TouchableOpacity
key={series}
style={[
styles.pickerItem,
formData.passport_serie === series && styles.selectedPickerItem
]}
onPress={() => {
updateFormData('passport_serie', series);
setShowPassportPicker(false);
}}
>
<Text style={[
styles.pickerItemText,
formData.passport_serie === series && styles.selectedPickerItemText
]}>
{series}
</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
</View>
</TouchableWithoutFeedback>
</Modal>
);
};
return (
<Modal
visible={visible}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={handleClose}
>
<SafeAreaView style={styles.container}>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.content}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={handleClose} style={styles.closeButton}>
<Ionicons name="close" size={24} color={COLORS.text} />
</TouchableOpacity>
<Text style={styles.title}>Şahsy maglumatlar</Text>
<View style={styles.placeholder} />
</View>
{/* Form */}
<ScrollView style={styles.form} showsVerticalScrollIndicator={false}>
<View style={styles.formSection}>
<Text style={styles.sectionTitle}>Esasy maglumatlar</Text>
<Input
label="Ady *"
value={formData.name}
onChangeText={(value) => updateFormData('name', value)}
error={errors.name}
maxLength={255}
returnKeyType="next"
onSubmitEditing={() => phoneInputRef.current?.focus()}
/>
<Input
ref={phoneInputRef}
label="Telefon belgisi *"
value={formData.phone}
onChangeText={(value) => updateFormData('phone', value)}
error={errors.phone}
keyboardType="numeric"
maxLength={8}
returnKeyType="next"
onSubmitEditing={() => passwordInputRef.current?.focus()}
/>
<Input
ref={passwordInputRef}
label="Täze parol"
value={formData.password}
onChangeText={(value) => updateFormData('password', value)}
error={errors.password}
secureTextEntry
placeholder="Parol üýtgetmezlik üçin boş goýuň"
returnKeyType="next"
onSubmitEditing={() => passportIdInputRef.current?.focus()}
/>
</View>
<View style={styles.formSection}>
<Text style={styles.sectionTitle}>Passport maglumatlary</Text>
<View style={styles.inputContainer}>
<Text style={styles.label}>Passport seriýasy</Text>
<TouchableOpacity
style={[styles.pickerButton, errors.passport_serie && styles.inputError]}
onPress={() => setShowPassportPicker(true)}
>
<Text style={[styles.pickerButtonText, !formData.passport_serie && styles.placeholderText]}>
{formData.passport_serie || 'Saýlaň'}
</Text>
<Ionicons name="chevron-down" size={20} color={COLORS.textSecondary} />
</TouchableOpacity>
{errors.passport_serie && (
<Text style={styles.errorText}>{errors.passport_serie}</Text>
)}
</View>
<Input
ref={passportIdInputRef}
label="Passport ID"
value={formData.passport_id}
onChangeText={(value) => updateFormData('passport_id', value)}
error={errors.passport_id}
keyboardType="numeric"
returnKeyType="done"
/>
</View>
<View style={styles.note}>
<Ionicons name="information-circle" size={16} color={COLORS.textSecondary} />
<Text style={styles.noteText}>
* belgisi bolan meýdanlar hökmany doldurulmaly
</Text>
</View>
</ScrollView>
{/* Save Button */}
<View style={styles.footer}>
<Button
title="Ýatda sakla"
onPress={handleSave}
disabled={isLoading}
style={styles.saveButton}
/>
{isLoading && (
<ActivityIndicator
size="small"
color={COLORS.primary}
style={styles.loader}
/>
)}
</View>
</View>
</TouchableWithoutFeedback>
{renderPassportSeriesPicker()}
</SafeAreaView>
</Modal>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: COLORS.background,
},
content: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: COLORS.border,
},
closeButton: {
padding: 4,
},
title: {
fontSize: 18,
fontWeight: 'bold',
color: COLORS.text,
},
placeholder: {
width: 32,
},
form: {
flex: 1,
paddingHorizontal: 20,
},
formSection: {
marginTop: 24,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
color: COLORS.text,
marginBottom: 16,
},
inputContainer: {
marginBottom: 16,
},
label: {
fontSize: 16,
fontWeight: '500',
color: COLORS.text,
marginBottom: 8,
},
pickerButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
borderWidth: 1,
borderColor: COLORS.border,
borderRadius: 8,
backgroundColor: COLORS.surface,
},
pickerButtonText: {
fontSize: 16,
color: COLORS.text,
},
placeholderText: {
color: COLORS.textSecondary,
},
inputError: {
borderColor: COLORS.error,
},
errorText: {
fontSize: 14,
color: COLORS.error,
marginTop: 4,
},
note: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 16,
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: COLORS.surface,
borderRadius: 8,
},
noteText: {
fontSize: 14,
color: COLORS.textSecondary,
marginLeft: 8,
flex: 1,
},
footer: {
padding: 20,
flexDirection: 'row',
alignItems: 'center',
},
saveButton: {
flex: 1,
},
loader: {
marginLeft: 12,
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
pickerContainer: {
backgroundColor: COLORS.surface,
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
maxHeight: 400,
},
pickerHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: COLORS.border,
},
pickerTitle: {
fontSize: 16,
fontWeight: '600',
color: COLORS.text,
},
pickerDoneText: {
fontSize: 16,
color: COLORS.primary,
fontWeight: '600',
},
pickerList: {
maxHeight: 300,
},
pickerItem: {
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: COLORS.border,
},
selectedPickerItem: {
backgroundColor: COLORS.primary + '10',
},
pickerItemText: {
fontSize: 16,
color: COLORS.text,
},
selectedPickerItemText: {
color: COLORS.primary,
fontWeight: '600',
},
});
export default EditProfileModal;

View File

@@ -35,4 +35,8 @@ export const COLORS = {
textSecondary: '#6b7280',
textDisabled: '#9ca3af',
textOnPrimary: '#ffffff',
text: '#111827',
// Additional semantic color
border: '#e5e7eb', // Same as gray.200
};

View File

@@ -14,6 +14,7 @@ import { StatusBar } from 'expo-status-bar';
import { Ionicons } from '@expo/vector-icons';
import { useAuth } from '../../contexts/AuthContext';
import apiService from '../../services/apiService';
import EditProfileModal from '../../components/EditProfileModal';
import { COLORS } from '../../constants/colors';
const ProfileScreen = () => {
@@ -21,6 +22,8 @@ const ProfileScreen = () => {
const [isLoading, setIsLoading] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [profileData, setProfileData] = useState(null);
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [isUpdatingProfile, setIsUpdatingProfile] = useState(false);
useEffect(() => {
loadProfileData();
@@ -108,8 +111,35 @@ const ProfileScreen = () => {
};
const handleEditPersonalInfo = () => {
// Navigate to edit personal info screen
Alert.alert('Şahsy maglumatlar', 'Şahsy maglumatlar sahypasy açylýar...');
setIsEditModalVisible(true);
};
const handleSaveProfile = async (updatedData) => {
try {
setIsUpdatingProfile(true);
const result = await apiService.updateProfile(updatedData);
if (result.success) {
// Update local profile data optimistically
setProfileData(prev => ({
...prev,
...updatedData,
}));
// Optionally refresh profile from server
await loadProfileData();
Alert.alert('Success', result.message);
setIsEditModalVisible(false);
} else {
Alert.alert('Error', result.error || 'Could not update profile');
}
} catch (error) {
console.error('Error updating profile:', error);
Alert.alert('Error', error.message || 'Could not update profile');
} finally {
setIsUpdatingProfile(false);
}
};
const handleEditContactInfo = () => {
@@ -295,6 +325,15 @@ const ProfileScreen = () => {
<View style={styles.bottomSpacing} />
</ScrollView>
{/* Edit Profile Modal */}
<EditProfileModal
visible={isEditModalVisible}
onClose={() => setIsEditModalVisible(false)}
onSave={handleSaveProfile}
initialData={profileData || user}
isLoading={isUpdatingProfile}
/>
</SafeAreaView>
);
};

View File

@@ -7,11 +7,35 @@ class ApiService {
}
async updateProfile(data) {
return authService.updateProfile(data);
try {
const response = await authService.updateProfile(data);
// Server returns { message: "Successfully updated profile" }
return {
success: true,
message: response.message || 'Profile updated successfully',
};
} catch (error) {
return {
success: false,
error: error.message,
};
}
}
async changePassword(currentPassword, newPassword) {
return authService.changePassword(currentPassword, newPassword);
try {
const response = await authService.changePassword(currentPassword, newPassword);
// Assume server returns { message: "Successfully changed password" }
return {
success: true,
message: response.message || 'Password changed successfully',
};
} catch (error) {
return {
success: false,
error: error.message,
};
}
}
async deleteAccount() {

View File

@@ -114,7 +114,7 @@ class AuthService {
}
async updateProfile(data) {
return this.makeRequest('/profile', data, true, 'PUT');
return this.makeRequest('/profile', data, true, 'POST');
}
async getTransactions(page = 1, limit = 20) {