diff --git a/src/components/EditProfileModal.js b/src/components/EditProfileModal.js
new file mode 100644
index 0000000..6a9fdaf
--- /dev/null
+++ b/src/components/EditProfileModal.js
@@ -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 (
+ setShowPassportPicker(false)}
+ >
+ setShowPassportPicker(false)}>
+
+
+
+ Passport seriýasy
+ setShowPassportPicker(false)}>
+ Boldy
+
+
+
+ {
+ updateFormData('passport_serie', '');
+ setShowPassportPicker(false);
+ }}
+ >
+
+ Saýlaň
+
+
+ {PASSPORT_SERIES.map((series) => (
+ {
+ updateFormData('passport_serie', series);
+ setShowPassportPicker(false);
+ }}
+ >
+
+ {series}
+
+
+ ))}
+
+
+
+
+
+ );
+ };
+
+ return (
+
+
+
+
+ {/* Header */}
+
+
+
+
+ Şahsy maglumatlar
+
+
+
+ {/* Form */}
+
+
+ Esasy maglumatlar
+
+ updateFormData('name', value)}
+ error={errors.name}
+ maxLength={255}
+ returnKeyType="next"
+ onSubmitEditing={() => phoneInputRef.current?.focus()}
+ />
+
+ updateFormData('phone', value)}
+ error={errors.phone}
+ keyboardType="numeric"
+ maxLength={8}
+ returnKeyType="next"
+ onSubmitEditing={() => passwordInputRef.current?.focus()}
+ />
+
+ updateFormData('password', value)}
+ error={errors.password}
+ secureTextEntry
+ placeholder="Parol üýtgetmezlik üçin boş goýuň"
+ returnKeyType="next"
+ onSubmitEditing={() => passportIdInputRef.current?.focus()}
+ />
+
+
+
+ Passport maglumatlary
+
+
+ Passport seriýasy
+ setShowPassportPicker(true)}
+ >
+
+ {formData.passport_serie || 'Saýlaň'}
+
+
+
+ {errors.passport_serie && (
+ {errors.passport_serie}
+ )}
+
+
+ updateFormData('passport_id', value)}
+ error={errors.passport_id}
+ keyboardType="numeric"
+ returnKeyType="done"
+ />
+
+
+
+
+
+ * belgisi bolan meýdanlar hökmany doldurulmaly
+
+
+
+
+ {/* Save Button */}
+
+
+ {isLoading && (
+
+ )}
+
+
+
+
+ {renderPassportSeriesPicker()}
+
+
+ );
+};
+
+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;
\ No newline at end of file
diff --git a/src/constants/colors.js b/src/constants/colors.js
index 4460005..41a1988 100644
--- a/src/constants/colors.js
+++ b/src/constants/colors.js
@@ -35,4 +35,8 @@ export const COLORS = {
textSecondary: '#6b7280',
textDisabled: '#9ca3af',
textOnPrimary: '#ffffff',
+ text: '#111827',
+
+ // Additional semantic color
+ border: '#e5e7eb', // Same as gray.200
};
\ No newline at end of file
diff --git a/src/screens/Main/ProfileScreen.js b/src/screens/Main/ProfileScreen.js
index b63c7e3..983751e 100644
--- a/src/screens/Main/ProfileScreen.js
+++ b/src/screens/Main/ProfileScreen.js
@@ -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 = () => {
+
+ {/* Edit Profile Modal */}
+ setIsEditModalVisible(false)}
+ onSave={handleSaveProfile}
+ initialData={profileData || user}
+ isLoading={isUpdatingProfile}
+ />
);
};
diff --git a/src/services/apiService.js b/src/services/apiService.js
index b5d7ed3..0665c1c 100644
--- a/src/services/apiService.js
+++ b/src/services/apiService.js
@@ -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() {
diff --git a/src/services/authService.js b/src/services/authService.js
index 8ff515b..7ee3763 100644
--- a/src/services/authService.js
+++ b/src/services/authService.js
@@ -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) {