Compare commits
34 Commits
89200eacd5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceabc0079c | ||
|
|
9b26ddaa37 | ||
|
|
0d12a01812 | ||
| 388d5e9a82 | |||
| 99bdb01644 | |||
| a41851dfcc | |||
|
|
27f43a77fd | ||
|
|
27f16f3c38 | ||
|
|
2eb41db2e5 | ||
|
|
61bcb6aa3f | ||
| 66f5271640 | |||
| db6a9e2bc0 | |||
| 14db29ab61 | |||
| 5ca3bb4f17 | |||
| 61fb0b2d96 | |||
| daae154aee | |||
| 6751cc506e | |||
| 056b4a5627 | |||
| d9b8e3f7ac | |||
| 5f59609d6f | |||
| b9b604167b | |||
| 4630b195b9 | |||
| 4efaf2543e | |||
| b925b48dd4 | |||
|
|
3380c9d85b | ||
| 6b9631dcfd | |||
| d4b81ee3e0 | |||
| 7f182d3a07 | |||
| dc4bef93c1 | |||
| 452887148d | |||
| 50d299fb24 | |||
| 8f16f14796 | |||
| 3eb5442a14 | |||
| f16e8c19b6 |
5
.gitignore
vendored
@@ -35,3 +35,8 @@ yarn-error.*
|
|||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
android/
|
||||||
|
ios/
|
||||||
|
.android/
|
||||||
|
.ios/
|
||||||
|
|||||||
7
App.js
@@ -2,15 +2,18 @@ import React from 'react';
|
|||||||
import { AuthProvider } from './src/contexts/AuthContext';
|
import { AuthProvider } from './src/contexts/AuthContext';
|
||||||
import RootNavigator from './src/navigation/RootNavigator';
|
import RootNavigator from './src/navigation/RootNavigator';
|
||||||
import { BaseEnumsProvider } from './src/contexts/BaseEnumsContext';
|
import { BaseEnumsProvider } from './src/contexts/BaseEnumsContext';
|
||||||
import { StatusBar } from 'react-native';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
<SafeAreaProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<BaseEnumsProvider>
|
<BaseEnumsProvider>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor="#fff"/>
|
<StatusBar style="dark" />
|
||||||
<RootNavigator />
|
<RootNavigator />
|
||||||
</BaseEnumsProvider>
|
</BaseEnumsProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</SafeAreaProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
26
app.json
@@ -10,10 +10,14 @@
|
|||||||
"splash": {
|
"splash": {
|
||||||
"image": "./assets/splash-icon.png",
|
"image": "./assets/splash-icon.png",
|
||||||
"resizeMode": "contain",
|
"resizeMode": "contain",
|
||||||
"backgroundColor": "#17b69b"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true
|
"supportsTablet": true,
|
||||||
|
"statusBar": {
|
||||||
|
"barStyle": "dark-content"
|
||||||
|
},
|
||||||
|
"bundleIdentifier": "com.nurmuhammet.ali.tbbankonline"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
@@ -21,7 +25,11 @@
|
|||||||
"backgroundColor": "#17b69b"
|
"backgroundColor": "#17b69b"
|
||||||
},
|
},
|
||||||
"edgeToEdgeEnabled": true,
|
"edgeToEdgeEnabled": true,
|
||||||
"package": "com.nurmuhammet.ali.tbbankonline"
|
"package": "com.nurmuhammet.ali.tbbankonline",
|
||||||
|
"statusBar": {
|
||||||
|
"barStyle": "dark-content",
|
||||||
|
"backgroundColor": "#ffffff"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"favicon": "./assets/favicon.png"
|
"favicon": "./assets/favicon.png"
|
||||||
@@ -30,6 +38,18 @@
|
|||||||
"eas": {
|
"eas": {
|
||||||
"projectId": "280bed78-9335-4b73-a686-15a9f726a7ad"
|
"projectId": "280bed78-9335-4b73-a686-15a9f726a7ad"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"expo-font",
|
||||||
|
[
|
||||||
|
"expo-splash-screen",
|
||||||
|
{
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"image": "./assets/splash-icon.png",
|
||||||
|
"imageWidth": 200
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"expo-system-ui"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.4 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 156 KiB |
787
package-lock.json
generated
23
package.json
@@ -4,28 +4,33 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"android": "expo start --android",
|
"android": "expo run:android",
|
||||||
"ios": "expo start --ios",
|
"ios": "expo run:ios",
|
||||||
"web": "expo start --web"
|
"web": "expo start --web"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^14.1.0",
|
"@expo/vector-icons": "^14.1.0",
|
||||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
"@react-native-async-storage/async-storage": "2.1.2",
|
||||||
"@react-native-community/datetimepicker": "^7.7.0",
|
"@react-native-community/datetimepicker": "8.4.1",
|
||||||
"@react-native-picker/picker": "^2.4.12",
|
"@react-native-picker/picker": "^2.4.12",
|
||||||
"@react-navigation/bottom-tabs": "^7.4.2",
|
"@react-navigation/bottom-tabs": "^7.4.2",
|
||||||
"@react-navigation/native": "^7.1.14",
|
"@react-navigation/native": "^7.1.14",
|
||||||
"@react-navigation/stack": "^7.4.2",
|
"@react-navigation/stack": "^7.4.2",
|
||||||
"expo": "~53.0.16",
|
"expo": "53.0.22",
|
||||||
|
"expo-font": "~13.3.2",
|
||||||
|
"expo-image-picker": "~16.1.4",
|
||||||
|
"expo-splash-screen": "^31.0.8",
|
||||||
"expo-status-bar": "~2.2.3",
|
"expo-status-bar": "~2.2.3",
|
||||||
|
"expo-system-ui": "~5.0.11",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-native": "0.79.5",
|
"react-native": "0.79.5",
|
||||||
|
"react-native-confirmation-code-field": "^8.0.1",
|
||||||
|
"react-native-gesture-handler": "~2.24.0",
|
||||||
"react-native-modal-datetime-picker": "^15.0.1",
|
"react-native-modal-datetime-picker": "^15.0.1",
|
||||||
"react-native-safe-area-context": "^5.5.1",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "^4.11.1",
|
"react-native-screens": "^4.11.1",
|
||||||
"react-native-svg": "^15.12.0",
|
"react-native-svg": "15.11.2",
|
||||||
"expo-image-picker": "~16.1.4",
|
"react-native-webview": "13.13.5"
|
||||||
"react-native-webview": "^13.11.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0"
|
"@babel/core": "^7.20.0"
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ const EditProfileModal = ({
|
|||||||
|
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
const [showPassportPicker, setShowPassportPicker] = useState(false);
|
const [showPassportPicker, setShowPassportPicker] = useState(false);
|
||||||
|
const [showMonthPicker, setShowMonthPicker] = useState(false);
|
||||||
|
const [showYearPicker, setShowYearPicker] = useState(false);
|
||||||
|
|
||||||
const phoneInputRef = useRef(null);
|
const phoneInputRef = useRef(null);
|
||||||
const passwordInputRef = useRef(null);
|
const passwordInputRef = useRef(null);
|
||||||
@@ -88,9 +90,9 @@ const EditProfileModal = ({
|
|||||||
newErrors.card_name = 'Kartyň ady 255 harpdan köp bolmaly däl';
|
newErrors.card_name = 'Kartyň ady 255 harpdan köp bolmaly däl';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Card number validation (optional, must be 16 digits)
|
// Card number validation (optional, must be 16 digits and allow hyphens)
|
||||||
if (formData.card_number && !/^\d{16}$/.test(formData.card_number.trim())) {
|
if (formData.card_number && !/^[0-9]{4}-?[0-9]{4}-?[0-9]{4}-?[0-9]{4}$/.test(formData.card_number.trim())) {
|
||||||
newErrors.card_number = 'Kart belgisi 16 sany sandan durmaly';
|
newErrors.card_number = 'Kart belgisi 16 sany sandan durmaly (mysal: 9999-9999-9999-9999)';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Card month validation (optional, 1-12)
|
// Card month validation (optional, 1-12)
|
||||||
@@ -102,9 +104,9 @@ const EditProfileModal = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Card year validation (optional, 4 digits and reasonable year)
|
// Card year validation (optional, 4 digits and reasonable year)
|
||||||
if (formData.card_year && !/^\d{4}$/.test(formData.card_year.trim())) {
|
// if (formData.card_year) {
|
||||||
newErrors.card_year = 'Ýyl 4 sany sandan durmaly';
|
|
||||||
}
|
// }
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
@@ -147,7 +149,8 @@ const EditProfileModal = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (formData.card_number) {
|
if (formData.card_number) {
|
||||||
updateData.card_number = formData.card_number.trim();
|
// Remove all non-digit characters before sending
|
||||||
|
updateData.card_number = formData.card_number.replace(/[^\d]/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
onSave(updateData);
|
onSave(updateData);
|
||||||
@@ -167,6 +170,7 @@ const EditProfileModal = ({
|
|||||||
card_number: initialData.card_number ? initialData.card_number.toString() : '',
|
card_number: initialData.card_number ? initialData.card_number.toString() : '',
|
||||||
});
|
});
|
||||||
setErrors({});
|
setErrors({});
|
||||||
|
Keyboard.dismiss(); // Dismiss keyboard when modal closes
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -244,6 +248,129 @@ const EditProfileModal = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderMonthPicker = () => {
|
||||||
|
if (!showMonthPicker) return null;
|
||||||
|
|
||||||
|
const months = Array.from({ length: 12 }, (_, i) => String(i + 1).padStart(2, '0'));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={showMonthPicker}
|
||||||
|
transparent={true}
|
||||||
|
animationType="slide"
|
||||||
|
onRequestClose={() => setShowMonthPicker(false)}
|
||||||
|
>
|
||||||
|
<TouchableWithoutFeedback onPress={() => setShowMonthPicker(false)}>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<View style={styles.pickerContainer}>
|
||||||
|
<View style={styles.pickerHeader}>
|
||||||
|
<Text style={styles.pickerTitle}>Kart aýy</Text>
|
||||||
|
<TouchableOpacity onPress={() => setShowMonthPicker(false)}>
|
||||||
|
<Text style={styles.pickerDoneText}>Boldy</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<ScrollView style={styles.pickerList}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.pickerItem}
|
||||||
|
onPress={() => {
|
||||||
|
updateFormData('card_month', '');
|
||||||
|
setShowMonthPicker(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={[styles.pickerItemText, styles.placeholderText]}>
|
||||||
|
Saýlaň
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
{months.map((month) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={month}
|
||||||
|
style={[
|
||||||
|
styles.pickerItem,
|
||||||
|
formData.card_month === month && styles.selectedPickerItem
|
||||||
|
]}
|
||||||
|
onPress={() => {
|
||||||
|
updateFormData('card_month', month);
|
||||||
|
setShowMonthPicker(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={[
|
||||||
|
styles.pickerItemText,
|
||||||
|
formData.card_month === month && styles.selectedPickerItemText
|
||||||
|
]}>
|
||||||
|
{month}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderYearPicker = () => {
|
||||||
|
if (!showYearPicker) return null;
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const years = Array.from({ length: 75 }, (_, i) => String(currentYear - 10 + i)); // +/- 10 years
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={showYearPicker}
|
||||||
|
transparent={true}
|
||||||
|
animationType="slide"
|
||||||
|
onRequestClose={() => setShowYearPicker(false)}
|
||||||
|
>
|
||||||
|
<TouchableWithoutFeedback onPress={() => setShowYearPicker(false)}>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<View style={styles.pickerContainer}>
|
||||||
|
<View style={styles.pickerHeader}>
|
||||||
|
<Text style={styles.pickerTitle}>Kartyň senesi</Text>
|
||||||
|
<TouchableOpacity onPress={() => setShowYearPicker(false)}>
|
||||||
|
<Text style={styles.pickerDoneText}>Boldy</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<ScrollView style={styles.pickerList}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.pickerItem}
|
||||||
|
onPress={() => {
|
||||||
|
updateFormData('card_year', '');
|
||||||
|
setShowYearPicker(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={[styles.pickerItemText, styles.placeholderText]}>
|
||||||
|
Saýlaň
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
{years.map((year) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={year}
|
||||||
|
style={[
|
||||||
|
styles.pickerItem,
|
||||||
|
formData.card_year === year && styles.selectedPickerItem
|
||||||
|
]}
|
||||||
|
onPress={() => {
|
||||||
|
updateFormData('card_year', year);
|
||||||
|
setShowYearPicker(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={[
|
||||||
|
styles.pickerItemText,
|
||||||
|
formData.card_year === year && styles.selectedPickerItemText
|
||||||
|
]}>
|
||||||
|
{year}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
@@ -256,7 +383,6 @@ const EditProfileModal = ({
|
|||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
>
|
>
|
||||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
@@ -268,7 +394,13 @@ const EditProfileModal = ({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<ScrollView style={styles.form} showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled" contentContainerStyle={{paddingBottom:80}}>
|
<ScrollView
|
||||||
|
style={styles.form}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
contentContainerStyle={{paddingBottom:80}}
|
||||||
|
keyboardDismissMode="on-drag"
|
||||||
|
>
|
||||||
<View style={styles.formSection}>
|
<View style={styles.formSection}>
|
||||||
<Text style={styles.sectionTitle}>Esasy maglumatlar</Text>
|
<Text style={styles.sectionTitle}>Esasy maglumatlar</Text>
|
||||||
|
|
||||||
@@ -334,6 +466,7 @@ const EditProfileModal = ({
|
|||||||
error={errors.passport_id}
|
error={errors.passport_id}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
|
maxLength={6}
|
||||||
onSubmitEditing={() => cardNameInputRef.current?.focus()}
|
onSubmitEditing={() => cardNameInputRef.current?.focus()}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -356,36 +489,55 @@ const EditProfileModal = ({
|
|||||||
ref={cardNumberInputRef}
|
ref={cardNumberInputRef}
|
||||||
label="Kart belgisi"
|
label="Kart belgisi"
|
||||||
value={formData.card_number}
|
value={formData.card_number}
|
||||||
onChangeText={(value) => updateFormData('card_number', value)}
|
onChangeText={(value) => {
|
||||||
|
const unmaskedValue = value.replace(/[^\d]/g, ''); // Remove non-digits
|
||||||
|
let maskedValue = '';
|
||||||
|
for (let i = 0; i < unmaskedValue.length; i++) {
|
||||||
|
if (i > 0 && i % 4 === 0) {
|
||||||
|
maskedValue += '-';
|
||||||
|
}
|
||||||
|
maskedValue += unmaskedValue[i];
|
||||||
|
}
|
||||||
|
updateFormData('card_number', maskedValue);
|
||||||
|
}}
|
||||||
error={errors.card_number}
|
error={errors.card_number}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
maxLength={16}
|
maxLength={19} // 16 digits + 3 hyphens
|
||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
onSubmitEditing={() => cardMonthInputRef.current?.focus()}
|
onSubmitEditing={() => cardMonthInputRef.current?.focus()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<View style={styles.inputContainer}>
|
||||||
ref={cardMonthInputRef}
|
<Text style={styles.label}>Kart aýy (MM)</Text>
|
||||||
label="Kart aýy (MM)"
|
<TouchableOpacity
|
||||||
value={formData.card_month}
|
style={[styles.pickerButton, errors.card_month && styles.inputError]}
|
||||||
onChangeText={(value) => updateFormData('card_month', value)}
|
onPress={() => setShowMonthPicker(true)}
|
||||||
error={errors.card_month}
|
>
|
||||||
keyboardType="numeric"
|
<Text style={[styles.pickerButtonText, !formData.card_month && styles.placeholderText]}>
|
||||||
maxLength={2}
|
{formData.card_month || 'Saýlaň'}
|
||||||
returnKeyType="next"
|
</Text>
|
||||||
onSubmitEditing={() => cardYearInputRef.current?.focus()}
|
<Ionicons name="chevron-down" size={20} color={COLORS.textSecondary} />
|
||||||
/>
|
</TouchableOpacity>
|
||||||
|
{errors.card_month && (
|
||||||
|
<Text style={styles.errorText}>{errors.card_month}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
<Input
|
<View style={styles.inputContainer}>
|
||||||
ref={cardYearInputRef}
|
<Text style={styles.label}>Kartyň senesi (YYYY)</Text>
|
||||||
label="Kartyň senesi (YYYY)"
|
<TouchableOpacity
|
||||||
value={formData.card_year}
|
style={[styles.pickerButton, errors.card_year && styles.inputError]}
|
||||||
onChangeText={(value) => updateFormData('card_year', value)}
|
onPress={() => setShowYearPicker(true)}
|
||||||
error={errors.card_year}
|
>
|
||||||
keyboardType="numeric"
|
<Text style={[styles.pickerButtonText, !formData.card_year && styles.placeholderText]}>
|
||||||
maxLength={4}
|
{formData.card_year || 'Saýlaň'}
|
||||||
returnKeyType="done"
|
</Text>
|
||||||
/>
|
<Ionicons name="chevron-down" size={20} color={COLORS.textSecondary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
{errors.card_year && (
|
||||||
|
<Text style={styles.errorText}>{errors.card_year}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.note}>
|
<View style={styles.note}>
|
||||||
@@ -413,10 +565,11 @@ const EditProfileModal = ({
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
|
||||||
{renderPassportSeriesPicker()}
|
{renderPassportSeriesPicker()}
|
||||||
|
{renderMonthPicker()}
|
||||||
|
{renderYearPicker()}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
132
src/components/TransactionList.js
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Text, FlatList, StyleSheet } from 'react-native';
|
||||||
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import { COLORS } from '../constants/colors';
|
||||||
|
|
||||||
|
const TransactionList = ({ transactions }) => {
|
||||||
|
if (!transactions || transactions.length === 0) {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.emptyText}>Soňky 10 günde kart hereketleri ýok...</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupedTransactions = transactions.reduce((acc, transaction) => {
|
||||||
|
const date = new Date(transaction.date).toLocaleDateString('tk', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
if (!acc[date]) {
|
||||||
|
acc[date] = [];
|
||||||
|
}
|
||||||
|
acc[date].push(transaction);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const renderTransactionItem = ({ item }) => (
|
||||||
|
<View style={styles.transactionItem}>
|
||||||
|
<View style={styles.transactionIcon}>
|
||||||
|
<Ionicons
|
||||||
|
name={item.type === 'credit' ? 'arrow-down' : 'arrow-up'}
|
||||||
|
size={20}
|
||||||
|
color={item.type === 'credit' ? COLORS.success : COLORS.danger}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.transactionDetails}>
|
||||||
|
<Text style={styles.transactionTitle}>{item.description}</Text>
|
||||||
|
<Text style={styles.transactionDate}>{item.time ?? '-'}</Text>
|
||||||
|
</View>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.transactionAmount,
|
||||||
|
{ color: item.type === 'credit' ? COLORS.success : COLORS.textPrimary },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.type === 'credit' ? '+' : '-'}
|
||||||
|
{item.amount} {item.currency}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTransactionSection = ({ item: date }) => (
|
||||||
|
<View>
|
||||||
|
<Text style={styles.transactionDateHeader}>{date}</Text>
|
||||||
|
<FlatList
|
||||||
|
data={groupedTransactions[date]}
|
||||||
|
renderItem={renderTransactionItem}
|
||||||
|
keyExtractor={(item) => item.id?.toString()}
|
||||||
|
scrollEnabled={false}
|
||||||
|
ItemSeparatorComponent={() => <View style={styles.separator} />}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
data={Object.keys(groupedTransactions)}
|
||||||
|
renderItem={renderTransactionSection}
|
||||||
|
keyExtractor={(date) => date}
|
||||||
|
scrollEnabled={false}
|
||||||
|
ItemSeparatorComponent={() => <View style={styles.separator} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
textAlign: 'center',
|
||||||
|
color: COLORS.textSecondary,
|
||||||
|
},
|
||||||
|
transactionItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
transactionIcon: {
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
borderRadius: 22,
|
||||||
|
backgroundColor: COLORS.background,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 16,
|
||||||
|
},
|
||||||
|
transactionDetails: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
transactionTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: COLORS.textPrimary,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
transactionDate: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: COLORS.textSecondary,
|
||||||
|
},
|
||||||
|
transactionAmount: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
transactionDateHeader: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: COLORS.textSecondary,
|
||||||
|
backgroundColor: COLORS.white,
|
||||||
|
paddingTop: 16,
|
||||||
|
paddingBottom: 8,
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: COLORS.gray[200],
|
||||||
|
marginLeft: 60,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TransactionList;
|
||||||
@@ -2,6 +2,8 @@ import React from 'react';
|
|||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { COLORS } from '../constants/colors';
|
import { COLORS } from '../constants/colors';
|
||||||
|
import { View, ActivityIndicator, Platform, OS } from 'react-native';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import HomeScreen from '../screens/Main/HomeScreen';
|
import HomeScreen from '../screens/Main/HomeScreen';
|
||||||
import MenuNavigator from './MenuNavigator';
|
import MenuNavigator from './MenuNavigator';
|
||||||
@@ -10,6 +12,8 @@ import ProfileScreen from '../screens/Main/ProfileScreen';
|
|||||||
const Tab = createBottomTabNavigator();
|
const Tab = createBottomTabNavigator();
|
||||||
|
|
||||||
const MainNavigator = () => {
|
const MainNavigator = () => {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
screenOptions={({ route }) => ({
|
screenOptions={({ route }) => ({
|
||||||
@@ -33,9 +37,17 @@ const MainNavigator = () => {
|
|||||||
backgroundColor: COLORS.white,
|
backgroundColor: COLORS.white,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: COLORS.gray[200],
|
borderTopColor: COLORS.gray[200],
|
||||||
paddingBottom: 8,
|
paddingBottom: (insets.bottom || 16),
|
||||||
paddingTop: 8,
|
paddingTop: 8,
|
||||||
height: 88,
|
height: Platform.OS === 'ios' ? 100 : (82 + (insets.bottom || 16)),
|
||||||
|
elevation: 8,
|
||||||
|
shadowColor: COLORS.gray[900],
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: -2,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 4,
|
||||||
},
|
},
|
||||||
tabBarLabelStyle: {
|
tabBarLabelStyle: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@@ -43,7 +55,7 @@ const MainNavigator = () => {
|
|||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
tabBarItemStyle: {
|
tabBarItemStyle: {
|
||||||
paddingVertical: 4,
|
paddingVertical: 2,
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createStackNavigator } from '@react-navigation/stack';
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import MenuScreen from '../screens/Main/MenuScreen';
|
import MenuScreen from '../screens/Main/MenuScreen';
|
||||||
import LoanRemainingOrdersScreen from '../screens/Loan/LoanRemainingOrdersScreen';
|
import LoanRemainingOrdersScreen from '../screens/Loan/LoanRemainingOrdersScreen';
|
||||||
import CreateLoanRemainingOrderScreen from '../screens/Loan/CreateLoanRemainingOrderScreen';
|
import CreateLoanRemainingOrderScreen from '../screens/Loan/CreateLoanRemainingOrderScreen';
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
SafeAreaView,
|
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Platform,
|
Platform,
|
||||||
@@ -12,6 +11,7 @@ import {
|
|||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import Button from '../../components/Button';
|
import Button from '../../components/Button';
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
SafeAreaView,
|
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Platform,
|
Platform,
|
||||||
@@ -11,6 +10,7 @@ import {
|
|||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import Button from '../../components/Button';
|
import Button from '../../components/Button';
|
||||||
|
|||||||
@@ -3,27 +3,40 @@ import {
|
|||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
SafeAreaView,
|
|
||||||
Alert,
|
Alert,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
BackHandler,
|
BackHandler,
|
||||||
Platform,
|
Platform,
|
||||||
|
KeyboardAvoidingView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import Button from '../../components/Button';
|
import Button from '../../components/Button';
|
||||||
import Input from '../../components/Input';
|
import {
|
||||||
|
CodeField,
|
||||||
|
Cursor,
|
||||||
|
useBlurOnFulfill,
|
||||||
|
useClearByFocusCell,
|
||||||
|
} from 'react-native-confirmation-code-field';
|
||||||
import { COLORS } from '../../constants/colors';
|
import { COLORS } from '../../constants/colors';
|
||||||
|
|
||||||
|
const CELL_COUNT = 6;
|
||||||
|
|
||||||
const VerificationScreen = ({ navigation }) => {
|
const VerificationScreen = ({ navigation }) => {
|
||||||
const [code, setCode] = useState('');
|
const [code, setCode] = useState('');
|
||||||
const [countdown, setCountdown] = useState(60);
|
const [countdown, setCountdown] = useState(60);
|
||||||
const [canResend, setCanResend] = useState(false);
|
const [canResend, setCanResend] = useState(false);
|
||||||
const { verify, isLoading, pendingVerification, clearPendingVerification } = useAuth();
|
const { verify, isLoading, pendingVerification, clearPendingVerification } = useAuth();
|
||||||
const countdownRef = useRef(null);
|
const countdownRef = useRef(null);
|
||||||
|
const ref = useBlurOnFulfill({ value: code, cellCount: CELL_COUNT });
|
||||||
|
const [props, getCellOnLayoutHandler] = useClearByFocusCell({
|
||||||
|
value: code,
|
||||||
|
setValue: setCode,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pendingVerification) {
|
if (!pendingVerification) {
|
||||||
@@ -73,18 +86,18 @@ const VerificationScreen = ({ navigation }) => {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVerify = async () => {
|
const handleVerify = async (filledCode) => {
|
||||||
if (!code.trim()) {
|
if (!filledCode.trim()) {
|
||||||
Alert.alert('Ýalňyşlyk', 'Tassyklama koduny giriziň');
|
Alert.alert('Ýalňyşlyk', 'Tassyklama koduny giriziň');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/^\d{6}$/.test(code.trim())) {
|
if (!/^\d{6}$/.test(filledCode.trim())) {
|
||||||
Alert.alert('Ýalňyşlyk', 'Tassyklama kody 6 sanly bolmaly');
|
Alert.alert('Ýalňyşlyk', 'Tassyklama kody 6 sanly bolmaly');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await verify(code.trim());
|
const result = await verify(filledCode.trim());
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Navigation will be handled by AuthContext
|
// Navigation will be handled by AuthContext
|
||||||
@@ -93,6 +106,8 @@ const VerificationScreen = ({ navigation }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleVerifyWrapper = () => handleVerify(code);
|
||||||
|
|
||||||
const handleResendCode = async () => {
|
const handleResendCode = async () => {
|
||||||
if (!canResend) return;
|
if (!canResend) return;
|
||||||
|
|
||||||
@@ -134,6 +149,10 @@ const VerificationScreen = ({ navigation }) => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
>
|
||||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<View style={styles.logoContainer}>
|
<View style={styles.logoContainer}>
|
||||||
@@ -148,22 +167,29 @@ const VerificationScreen = ({ navigation }) => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.formContainer}>
|
<View style={styles.formContainer}>
|
||||||
<Input
|
<CodeField
|
||||||
label="Tassyklama kody"
|
ref={ref}
|
||||||
|
{...props}
|
||||||
value={code}
|
value={code}
|
||||||
onChangeText={setCode}
|
onChangeText={setCode}
|
||||||
placeholder="123456"
|
cellCount={CELL_COUNT}
|
||||||
keyboardType="numeric"
|
rootStyle={styles.otpContainer}
|
||||||
maxLength={6}
|
keyboardType="number-pad"
|
||||||
style={styles.codeInput}
|
textContentType="oneTimeCode"
|
||||||
textAlign="center"
|
renderCell={({ index, symbol, isFocused }) => (
|
||||||
returnKeyType="done"
|
<Text
|
||||||
onSubmitEditing={handleVerify}
|
key={index}
|
||||||
|
style={[styles.otpInput, isFocused && styles.otpInputHighlight]}
|
||||||
|
onLayout={getCellOnLayoutHandler(index)}>
|
||||||
|
{symbol || (isFocused ? <Cursor/> : null)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
onSubmitEditing={handleVerifyWrapper}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
title="Tassykla"
|
title="Tassykla"
|
||||||
onPress={handleVerify}
|
onPress={handleVerifyWrapper}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
style={styles.verifyButton}
|
style={styles.verifyButton}
|
||||||
/>
|
/>
|
||||||
@@ -182,6 +208,7 @@ const VerificationScreen = ({ navigation }) => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -251,10 +278,25 @@ const styles = StyleSheet.create({
|
|||||||
formContainer: {
|
formContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
codeInput: {
|
otpContainer: {
|
||||||
width: '100%',
|
width: '90%',
|
||||||
marginBottom: 32,
|
marginBottom: 32,
|
||||||
},
|
},
|
||||||
|
otpInput: {
|
||||||
|
width: 45,
|
||||||
|
height: 60,
|
||||||
|
lineHeight: 58,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 12,
|
||||||
|
borderColor: COLORS.gray[300],
|
||||||
|
color: COLORS.textPrimary,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
otpInputHighlight: {
|
||||||
|
borderColor: COLORS.primary,
|
||||||
|
},
|
||||||
verifyButton: {
|
verifyButton: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
marginBottom: 32,
|
marginBottom: 32,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, SafeAreaView, Modal } from 'react-native';
|
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, Modal } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||||
import apiService from '../../services/apiService';
|
import apiService from '../../services/apiService';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl, SafeAreaView } from 'react-native';
|
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||||
import apiService from '../../services/apiService';
|
import apiService from '../../services/apiService';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, ScrollView, SafeAreaView, Image, Alert, Linking } from 'react-native';
|
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, ScrollView, Image, Alert, Linking } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator, RefreshControl, SafeAreaView } from 'react-native';
|
import { View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator, RefreshControl } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
@@ -7,8 +8,8 @@ import apiService from '../../services/apiService';
|
|||||||
import { COLORS } from '../../constants/colors';
|
import { COLORS } from '../../constants/colors';
|
||||||
import { useBaseEnums } from '../../contexts/BaseEnumsContext';
|
import { useBaseEnums } from '../../contexts/BaseEnumsContext';
|
||||||
|
|
||||||
const CARD_BG = '#EFF6FF';
|
const CARD_BG = '#F1F9F1';
|
||||||
const CIRCLE_BG = '#7FB3FF';
|
const CIRCLE_BG = '#A2E4A4';
|
||||||
|
|
||||||
const CardOrdersScreen = () => {
|
const CardOrdersScreen = () => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, SafeAreaView, Image, Linking } from 'react-native';
|
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, Image, Linking } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl, SafeAreaView } from 'react-native';
|
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, SafeAreaView, Image } from 'react-native';
|
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, Image } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
@@ -137,6 +138,9 @@ const CardRequisiteOrderDetailsScreen = () => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
|
<TouchableOpacity style={styles.actionBtn} onPress={handleDownload}>
|
||||||
|
<Text style={styles.actionText}>Ýükle (PDF)</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={styles.deleteBtn} onPress={handleDelete}>
|
<TouchableOpacity style={styles.deleteBtn} onPress={handleDelete}>
|
||||||
<Text style={styles.deleteText}>Poz</Text>
|
<Text style={styles.deleteText}>Poz</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useCallback, useRef } from 'react';
|
import React, { useState, useCallback, useRef } from 'react';
|
||||||
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl, SafeAreaView } from 'react-native';
|
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, SafeAreaView, Modal } from 'react-native';
|
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, Modal } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||||
import apiService from '../../services/apiService';
|
import apiService from '../../services/apiService';
|
||||||
@@ -93,7 +94,7 @@ const CardTransactionOrderDetailsScreen = () => {
|
|||||||
if (!order) {
|
if (!order) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centered}>
|
<View style={styles.centered}>
|
||||||
<Text>No data</Text>
|
<Text>Maglumat tapylmady</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl, SafeAreaView } from 'react-native';
|
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||||
import apiService from '../../services/apiService';
|
import apiService from '../../services/apiService';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, SafeAreaView, ScrollView } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, ScrollView } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import apiService from '../../services/apiService';
|
import apiService from '../../services/apiService';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, SafeAreaView, ScrollView, Modal } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, ScrollView, Modal } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, SafeAreaView, ScrollView } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, ScrollView } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, SafeAreaView, ScrollView } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, ScrollView } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, SafeAreaView, ScrollView } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, ScrollView } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import apiService from '../../services/apiService';
|
import apiService from '../../services/apiService';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, ScrollView, SafeAreaView, View } from 'react-native';
|
import { Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, ScrollView, View } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { COLORS } from '../../constants/colors';
|
import { COLORS } from '../../constants/colors';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, ScrollView, SafeAreaView } from 'react-native';
|
import { Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, ScrollView } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { COLORS } from '../../constants/colors';
|
import { COLORS } from '../../constants/colors';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, SafeAreaView } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import apiService from '../../services/apiService';
|
import apiService from '../../services/apiService';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, SafeAreaView } from 'react-native';
|
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||||
import apiService from '../../services/apiService';
|
import apiService from '../../services/apiService';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl, SafeAreaView } from 'react-native';
|
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl, Alert } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||||
import apiService from '../../services/apiService';
|
import apiService from '../../services/apiService';
|
||||||
@@ -47,6 +48,32 @@ const LoanOrdersScreen = () => {
|
|||||||
const amount = (item.loan_amount + ' TMT') || '-';
|
const amount = (item.loan_amount + ' TMT') || '-';
|
||||||
const created = item.created_at ? new Date(item.created_at).toLocaleDateString() : '';
|
const created = item.created_at ? new Date(item.created_at).toLocaleDateString() : '';
|
||||||
|
|
||||||
|
const handleDelete = async (orderId) => {
|
||||||
|
Alert.alert(
|
||||||
|
"Sargyty pozmak",
|
||||||
|
"Siz hakykatdanam bu sargyty pozmak isleýärsiňizmi?",
|
||||||
|
[
|
||||||
|
{ text: "Ýok", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Hawa",
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiService.deleteLoanOrder(orderId);
|
||||||
|
if (res.success) {
|
||||||
|
setOrders(prevOrders => prevOrders.filter(order => order.id !== orderId));
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to delete order:', res.error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error deleting order:', e.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ cancelable: true }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity style={styles.card} onPress={() => navigation.navigate('LoanOrderDetails', { orderId: item.id })}>
|
<TouchableOpacity style={styles.card} onPress={() => navigation.navigate('LoanOrderDetails', { orderId: item.id })}>
|
||||||
<View style={styles.circle}>
|
<View style={styles.circle}>
|
||||||
@@ -58,8 +85,11 @@ const LoanOrdersScreen = () => {
|
|||||||
<Text style={{ color: COLORS.textPrimary }}>{amount}</Text>
|
<Text style={{ color: COLORS.textPrimary }}>{amount}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text style={styles.dateText}>{created}</Text>
|
<Text style={styles.dateText}>Döredilen senesi: {created}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<TouchableOpacity onPress={() => handleDelete(item.id)} style={styles.deleteButton}>
|
||||||
|
<Ionicons name="trash" size={24} color={COLORS.error} />
|
||||||
|
</TouchableOpacity>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -112,6 +142,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
},
|
},
|
||||||
circle: {
|
circle: {
|
||||||
width: 40,
|
width: 40,
|
||||||
@@ -127,7 +158,7 @@ const styles = StyleSheet.create({
|
|||||||
passportText: { fontWeight: '700', color: COLORS.textPrimary, marginBottom: 4 },
|
passportText: { fontWeight: '700', color: COLORS.textPrimary, marginBottom: 4 },
|
||||||
accountLabel: { color: COLORS.textSecondary, fontSize: 14 },
|
accountLabel: { color: COLORS.textSecondary, fontSize: 14 },
|
||||||
accountValue: { color: COLORS.textPrimary, marginBottom: 4 },
|
accountValue: { color: COLORS.textPrimary, marginBottom: 4 },
|
||||||
dateText: { color: COLORS.textSecondary, fontSize: 12, textAlign: 'right' },
|
dateText: { color: COLORS.textSecondary, fontSize: 12, textAlign: 'left' },
|
||||||
emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||||
emptyText: { fontSize: 16, color: COLORS.textSecondary },
|
emptyText: { fontSize: 16, color: COLORS.textSecondary },
|
||||||
fab: {
|
fab: {
|
||||||
@@ -142,6 +173,9 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
},
|
},
|
||||||
|
deleteButton: {
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default LoanOrdersScreen;
|
export default LoanOrdersScreen;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, SafeAreaView } from 'react-native';
|
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||||
import apiService from '../../services/apiService';
|
import apiService from '../../services/apiService';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl, SafeAreaView } from 'react-native';
|
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl, Alert } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||||
import apiService from '../../services/apiService';
|
import apiService from '../../services/apiService';
|
||||||
@@ -47,6 +48,32 @@ const LoanPaidOffLetterOrdersScreen = () => {
|
|||||||
const accountLine = `Karz hasaby:`;
|
const accountLine = `Karz hasaby:`;
|
||||||
const created = item.created_at ? new Date(item.created_at).toLocaleDateString() : '';
|
const created = item.created_at ? new Date(item.created_at).toLocaleDateString() : '';
|
||||||
|
|
||||||
|
const handleDelete = async (orderId) => {
|
||||||
|
Alert.alert(
|
||||||
|
"Sargyty pozmak",
|
||||||
|
"Siz hakykatdanam bu sargyty pozmak isleýärsiňizmi?",
|
||||||
|
[
|
||||||
|
{ text: "Ýok", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Hawa",
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiService.deleteLoanPaidOffLetterOrder(orderId);
|
||||||
|
if (res.success) {
|
||||||
|
setOrders(prevOrders => prevOrders.filter(order => order.id !== orderId));
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to delete order:', res.error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error deleting order:', e.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ cancelable: true }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity style={styles.card} onPress={() => navigation.navigate('LoanPaidOffLetterOrderDetails', { orderId: item.id })}>
|
<TouchableOpacity style={styles.card} onPress={() => navigation.navigate('LoanPaidOffLetterOrderDetails', { orderId: item.id })}>
|
||||||
<View style={styles.circle}>
|
<View style={styles.circle}>
|
||||||
@@ -58,6 +85,9 @@ const LoanPaidOffLetterOrdersScreen = () => {
|
|||||||
<Text style={styles.accountValue}>{item.loan_contract_number || '-'}</Text>
|
<Text style={styles.accountValue}>{item.loan_contract_number || '-'}</Text>
|
||||||
<Text style={styles.dateText}>{created}</Text>
|
<Text style={styles.dateText}>{created}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<TouchableOpacity onPress={() => handleDelete(item.id)} style={styles.deleteButton}>
|
||||||
|
<Ionicons name="trash" size={24} color={COLORS.error} />
|
||||||
|
</TouchableOpacity>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -110,6 +140,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
},
|
},
|
||||||
circle: {
|
circle: {
|
||||||
width: 40,
|
width: 40,
|
||||||
@@ -140,6 +171,9 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
},
|
},
|
||||||
|
deleteButton: {
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default LoanPaidOffLetterOrdersScreen;
|
export default LoanPaidOffLetterOrdersScreen;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert } from 'react-native';
|
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||||
import apiService from '../../services/apiService';
|
import apiService from '../../services/apiService';
|
||||||
@@ -63,7 +64,7 @@ const LoanRemainingOrderDetailsScreen = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<TouchableOpacity style={styles.backBtn} onPress={() => navigation.goBack()}>
|
<TouchableOpacity style={styles.backBtn} onPress={() => navigation.goBack()}>
|
||||||
<Ionicons name="close" size={28} color={COLORS.textPrimary} />
|
<Ionicons name="close" size={28} color={COLORS.textPrimary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -82,7 +83,7 @@ const LoanRemainingOrderDetailsScreen = () => {
|
|||||||
<TouchableOpacity style={styles.deleteBtn} onPress={handleDelete}>
|
<TouchableOpacity style={styles.deleteBtn} onPress={handleDelete}>
|
||||||
<Text style={styles.deleteText}>Poz</Text>
|
<Text style={styles.deleteText}>Poz</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl, Modal, ScrollView, Alert, SafeAreaView, Pressable } from 'react-native';
|
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl, Modal, ScrollView, Alert, Pressable } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||||
import apiService from '../../services/apiService';
|
import apiService from '../../services/apiService';
|
||||||
@@ -65,6 +66,32 @@ const LoanRemainingOrdersScreen = () => {
|
|||||||
const accountLabel = 'Karz hasaby:';
|
const accountLabel = 'Karz hasaby:';
|
||||||
const created = item.created_at ? new Date(item.created_at).toLocaleDateString() : '';
|
const created = item.created_at ? new Date(item.created_at).toLocaleDateString() : '';
|
||||||
|
|
||||||
|
const handleDelete = async (orderId) => {
|
||||||
|
Alert.alert(
|
||||||
|
"Sargyty pozmak",
|
||||||
|
"Siz hakykatdanam bu sargyty pozmak isleýärsiňizmi?",
|
||||||
|
[
|
||||||
|
{ text: "Ýok", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Hawa",
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiService.deleteLoanRemainingOrder(orderId);
|
||||||
|
if (res.success) {
|
||||||
|
setOrders(prevOrders => prevOrders.filter(order => order.id !== orderId));
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to delete order:', res.error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error deleting order:', e.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ cancelable: true }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity style={styles.card} onPress={() => handleItemPress(item)}>
|
<TouchableOpacity style={styles.card} onPress={() => handleItemPress(item)}>
|
||||||
<View style={styles.circle}>
|
<View style={styles.circle}>
|
||||||
@@ -74,8 +101,11 @@ const LoanRemainingOrdersScreen = () => {
|
|||||||
<Text style={styles.passportText}>{passportLine}</Text>
|
<Text style={styles.passportText}>{passportLine}</Text>
|
||||||
<Text style={styles.accountLabel}>{accountLabel}</Text>
|
<Text style={styles.accountLabel}>{accountLabel}</Text>
|
||||||
<Text style={styles.accountValue}>{item.account_number}</Text>
|
<Text style={styles.accountValue}>{item.account_number}</Text>
|
||||||
<Text style={styles.dateText}>{created}</Text>
|
<Text style={styles.dateText}>Döredilen senesi: {created}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<TouchableOpacity onPress={() => handleDelete(item.id)} style={styles.deleteButton}>
|
||||||
|
<Ionicons name="trash" size={24} color={COLORS.error} />
|
||||||
|
</TouchableOpacity>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -104,7 +134,7 @@ const LoanRemainingOrdersScreen = () => {
|
|||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
contentContainerStyle={orders.length === 0 && styles.emptyContainer}
|
contentContainerStyle={orders.length === 0 && styles.emptyContainer}
|
||||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
||||||
ListEmptyComponent={<Text style={styles.emptyText}>No orders yet</Text>}
|
ListEmptyComponent={<Text style={styles.emptyText}>Sargyt ýok</Text>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Floating Action Button */}
|
{/* Floating Action Button */}
|
||||||
@@ -204,6 +234,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between', // Added to push delete button to the right
|
||||||
},
|
},
|
||||||
circle: {
|
circle: {
|
||||||
width: 40,
|
width: 40,
|
||||||
@@ -318,6 +349,9 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: COLORS.textPrimary,
|
color: COLORS.textPrimary,
|
||||||
},
|
},
|
||||||
|
deleteButton: {
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default LoanRemainingOrdersScreen;
|
export default LoanRemainingOrdersScreen;
|
||||||
@@ -3,26 +3,116 @@ import {
|
|||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
SafeAreaView,
|
|
||||||
ScrollView,
|
ScrollView,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
RefreshControl,
|
||||||
|
FlatList,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { COLORS } from '../../constants/colors';
|
import { COLORS } from '../../constants/colors';
|
||||||
import MetricCard from '../../components/MetricCard';
|
import MetricCard from '../../components/MetricCard';
|
||||||
import apiService from '../../services/apiService';
|
import apiService from '../../services/apiService';
|
||||||
|
import TransactionList from '../../components/TransactionList';
|
||||||
|
|
||||||
|
const STATIC_TRANSACTIONS = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 'debit',
|
||||||
|
description: 'Canva Design and Publishing',
|
||||||
|
date: '2025-09-07T10:30:00Z',
|
||||||
|
amount: 10.0,
|
||||||
|
currency: 'EUR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: 'debit',
|
||||||
|
description: 'Google',
|
||||||
|
date: '2025-09-07T11:00:00Z',
|
||||||
|
amount: 12.99,
|
||||||
|
currency: 'SAR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
type: 'debit',
|
||||||
|
description: 'Canva Design and Publishing',
|
||||||
|
date: '2025-09-05T14:15:00Z',
|
||||||
|
amount: 10.0,
|
||||||
|
currency: 'EUR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
type: 'credit',
|
||||||
|
description: 'Töleg alyňýan hasap',
|
||||||
|
date: '2025-09-03T09:00:00Z',
|
||||||
|
amount: 20.0,
|
||||||
|
currency: 'USD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
type: 'debit',
|
||||||
|
description: 'Комиссия за оплату',
|
||||||
|
date: '2025-09-03T09:01:00Z',
|
||||||
|
amount: 0.12,
|
||||||
|
currency: 'USD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
type: 'credit',
|
||||||
|
description: 'Hasaby doldurmak',
|
||||||
|
date: '2025-09-02T18:45:00Z',
|
||||||
|
amount: 10.0,
|
||||||
|
currency: 'USD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
type: 'debit',
|
||||||
|
description: 'Canva Design and Publishing',
|
||||||
|
date: '2025-09-01T12:00:00Z',
|
||||||
|
amount: 10.0,
|
||||||
|
currency: 'EUR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
type: 'debit',
|
||||||
|
description: 'Netflix',
|
||||||
|
date: '2025-08-28T16:00:00Z',
|
||||||
|
amount: 9.99,
|
||||||
|
currency: 'USD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
type: 'credit',
|
||||||
|
description: 'Bank Transfer',
|
||||||
|
date: '2025-08-25T08:20:00Z',
|
||||||
|
amount: 500.0,
|
||||||
|
currency: 'TMT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
type: 'debit',
|
||||||
|
description: 'Amazon',
|
||||||
|
date: '2025-08-23T19:55:00Z',
|
||||||
|
amount: 45.5,
|
||||||
|
currency: 'USD',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const HomeScreen = () => {
|
const HomeScreen = () => {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
const [metrics, setMetrics] = useState(null);
|
const [metrics, setMetrics] = useState(null);
|
||||||
const [loadingMetrics, setLoadingMetrics] = useState(true);
|
const [loadingMetrics, setLoadingMetrics] = useState(true);
|
||||||
const [cardBalance, setCardBalance] = useState(null);
|
const [cardBalance, setCardBalance] = useState(null);
|
||||||
const [loadingCardBalance, setLoadingCardBalance] = useState(true);
|
const [loadingCardBalance, setLoadingCardBalance] = useState(true);
|
||||||
const [cardBalanceError, setCardBalanceError] = useState(null);
|
const [cardBalanceError, setCardBalanceError] = useState(null);
|
||||||
const [isBalanceVisible, setIsBalanceVisible] = useState(true);
|
const [isBalanceVisible, setIsBalanceVisible] = useState(true);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [transactions, setTransactions] = useState([]);
|
||||||
|
const [loadingTransactions, setLoadingTransactions] = useState(true);
|
||||||
|
|
||||||
const showBalanceCard = !loadingCardBalance && cardBalanceError === null && cardBalance !== null;
|
const showBalanceCard = !loadingCardBalance && cardBalanceError === null && cardBalance !== null;
|
||||||
|
|
||||||
@@ -48,16 +138,20 @@ const HomeScreen = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCardBalance = async () => {
|
const fetchCardData = async () => {
|
||||||
// Ensure user has filled all required card & passport fields
|
// Ensure user has filled all required card & passport fields
|
||||||
if (!user?.passport_serie || !user?.passport_id || !user?.card_number || !user?.card_month || !user?.card_year) {
|
if (!user?.passport_serie || !user?.passport_id || !user?.card_number || !user?.card_month || !user?.card_year) {
|
||||||
setLoadingCardBalance(false);
|
setLoadingCardBalance(false);
|
||||||
|
setLoadingTransactions(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoadingCardBalance(true);
|
||||||
|
setLoadingTransactions(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await apiService.getCardBalanceQuickCheck();
|
const res = await apiService.getCardBalanceQuickCheck();
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
// Try common balance keys else fallback to raw
|
|
||||||
const raw = res.data;
|
const raw = res.data;
|
||||||
let balanceValue = null;
|
let balanceValue = null;
|
||||||
if (raw && typeof raw === 'object') {
|
if (raw && typeof raw === 'object') {
|
||||||
@@ -71,21 +165,102 @@ const HomeScreen = () => {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Error fetching card balance:', e);
|
console.warn('Error fetching card balance:', e);
|
||||||
|
setCardBalanceError('Error fetching balance');
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingCardBalance(false);
|
setLoadingCardBalance(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const transRes = await apiService.getCardTransactionsLastMonth();
|
||||||
|
if (transRes.success) {
|
||||||
|
setTransactions(transRes.data || []);
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to fetch transactions:', transRes.error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error fetching transactions:', e);
|
||||||
|
} finally {
|
||||||
|
setLoadingTransactions(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchCardBalance();
|
fetchCardData();
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
await Promise.all([
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiService.getMetrics();
|
||||||
|
if (response.success) {
|
||||||
|
setMetrics(response.data);
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to fetch metrics during refresh:', response.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error fetching metrics during refresh:', error);
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
(async () => {
|
||||||
|
if (!user?.passport_serie || !user?.passport_id || !user?.card_number || !user?.card_month || !user?.card_year) {
|
||||||
|
setCardBalance(null); // Clear previous balance if conditions are not met
|
||||||
|
setCardBalanceError(null);
|
||||||
|
setTransactions([]);
|
||||||
|
setLoadingTransactions(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await apiService.getCardBalanceQuickCheck();
|
||||||
|
if (res.success) {
|
||||||
|
const raw = res.data;
|
||||||
|
let balanceValue = null;
|
||||||
|
if (raw && typeof raw === 'object') {
|
||||||
|
balanceValue = raw.balance ?? raw.card_balance ?? raw.amount ?? null;
|
||||||
|
}
|
||||||
|
setCardBalance(balanceValue);
|
||||||
|
setCardBalanceError(null);
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to fetch card balance during refresh:', res.error);
|
||||||
|
setCardBalanceError(res.error || 'Error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error fetching card balance during refresh:', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const transRes = await apiService.getCardTransactionsLastMonth();
|
||||||
|
if (transRes.success) {
|
||||||
|
setTransactions(transRes.data || []);
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to fetch transactions during refresh:', transRes.error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error fetching transactions during refresh:', e);
|
||||||
|
} finally {
|
||||||
|
setLoadingTransactions(false);
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
]);
|
||||||
|
setRefreshing(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<View style={styles.container}>
|
||||||
<StatusBar style="dark" />
|
<StatusBar style="dark" />
|
||||||
|
|
||||||
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
|
<ScrollView
|
||||||
|
style={styles.scrollView}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={COLORS.primary}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={[styles.header, { paddingTop: insets.top + 16 }]}>
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.greeting}>Salam,</Text>
|
<Text style={styles.greeting}>Salam,</Text>
|
||||||
<Text style={styles.userName}>{user?.name || 'Ulanyjy'}</Text>
|
<Text style={styles.userName}>{user?.name || 'Ulanyjy'}</Text>
|
||||||
@@ -94,7 +269,7 @@ const HomeScreen = () => {
|
|||||||
|
|
||||||
{/* Metrics */}
|
{/* Metrics */}
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={styles.sectionTitle}>Metrics</Text>
|
<Text style={styles.sectionTitle}>Görkezijiler</Text>
|
||||||
{loadingMetrics ? (
|
{loadingMetrics ? (
|
||||||
<ActivityIndicator color={COLORS.primary} style={{ marginTop: 16 }} />
|
<ActivityIndicator color={COLORS.primary} style={{ marginTop: 16 }} />
|
||||||
) : (
|
) : (
|
||||||
@@ -126,8 +301,27 @@ const HomeScreen = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Card Transactions */}
|
||||||
|
{showBalanceCard && (
|
||||||
|
<View style={styles.section}>
|
||||||
|
<View style={styles.sectionHeader}>
|
||||||
|
<Text style={styles.sectionTitle}>Kart hereketleri</Text>
|
||||||
|
<Text style={styles.sectionSubtitle}>Soňky 10 günde</Text>
|
||||||
|
|
||||||
|
<TouchableOpacity>
|
||||||
|
{/* <Text style={styles.seeAllText}>Hemmesi</Text> */}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
{loadingTransactions ? (
|
||||||
|
<ActivityIndicator color={COLORS.primary} style={{ marginTop: 16 }} />
|
||||||
|
) : (
|
||||||
|
<TransactionList transactions={transactions} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -144,7 +338,6 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 24,
|
paddingHorizontal: 24,
|
||||||
paddingTop: 16,
|
|
||||||
paddingBottom: 24,
|
paddingBottom: 24,
|
||||||
backgroundColor: COLORS.white,
|
backgroundColor: COLORS.white,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
@@ -218,16 +411,16 @@ const styles = StyleSheet.create({
|
|||||||
padding: 20,
|
padding: 20,
|
||||||
},
|
},
|
||||||
sectionHeader: {
|
sectionHeader: {
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: COLORS.textPrimary,
|
color: COLORS.textPrimary,
|
||||||
marginBottom: 16,
|
},
|
||||||
|
sectionSubtitle: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: COLORS.textSecondary,
|
||||||
},
|
},
|
||||||
seeAllText: {
|
seeAllText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -255,58 +448,6 @@ const styles = StyleSheet.create({
|
|||||||
color: COLORS.textSecondary,
|
color: COLORS.textSecondary,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
transactionsList: {
|
|
||||||
gap: 16,
|
|
||||||
},
|
|
||||||
transactionItem: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
transactionIcon: {
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
borderRadius: 20,
|
|
||||||
backgroundColor: COLORS.backgroundSecondary,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginRight: 12,
|
|
||||||
},
|
|
||||||
transactionDetails: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
transactionTitle: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: COLORS.textPrimary,
|
|
||||||
},
|
|
||||||
transactionDate: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: COLORS.textSecondary,
|
|
||||||
},
|
|
||||||
transactionAmount: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: COLORS.success,
|
|
||||||
},
|
|
||||||
servicesGrid: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
},
|
|
||||||
serviceItem: {
|
|
||||||
width: '48%',
|
|
||||||
padding: 16,
|
|
||||||
backgroundColor: COLORS.backgroundSecondary,
|
|
||||||
borderRadius: 12,
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
serviceText: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: COLORS.textSecondary,
|
|
||||||
textAlign: 'center',
|
|
||||||
marginTop: 8,
|
|
||||||
},
|
|
||||||
metricsGrid: {
|
metricsGrid: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
|||||||
@@ -4,16 +4,17 @@ import {
|
|||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
SafeAreaView,
|
|
||||||
ScrollView,
|
ScrollView,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { COLORS } from '../../constants/colors';
|
import { COLORS } from '../../constants/colors';
|
||||||
|
|
||||||
const MenuScreen = () => {
|
const MenuScreen = () => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
const menuSections = [
|
const menuSections = [
|
||||||
{
|
{
|
||||||
@@ -66,10 +67,10 @@ const MenuScreen = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<View style={styles.container}>
|
||||||
<StatusBar style="dark" />
|
<StatusBar style="dark" />
|
||||||
|
|
||||||
<View style={styles.header}>
|
<View style={[styles.header, { paddingTop: insets.top + 16 }]}>
|
||||||
<Text style={styles.headerTitle}>Hyzmatlar</Text>
|
<Text style={styles.headerTitle}>Hyzmatlar</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -105,7 +106,7 @@ const MenuScreen = () => {
|
|||||||
|
|
||||||
<View style={styles.bottomSpacing} />
|
<View style={styles.bottomSpacing} />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -116,7 +117,6 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
paddingHorizontal: 24,
|
paddingHorizontal: 24,
|
||||||
paddingTop: 16,
|
|
||||||
paddingBottom: 24,
|
paddingBottom: 24,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: COLORS.gray[200],
|
borderBottomColor: COLORS.gray[200],
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ import {
|
|||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
SafeAreaView,
|
|
||||||
ScrollView,
|
ScrollView,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Alert,
|
Alert,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
|
Linking,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import apiService from '../../services/apiService';
|
import apiService from '../../services/apiService';
|
||||||
import EditProfileModal from '../../components/EditProfileModal';
|
import EditProfileModal from '../../components/EditProfileModal';
|
||||||
@@ -19,6 +20,7 @@ import { COLORS } from '../../constants/colors';
|
|||||||
|
|
||||||
const ProfileScreen = () => {
|
const ProfileScreen = () => {
|
||||||
const { user, logout, fetchUserProfile } = useAuth();
|
const { user, logout, fetchUserProfile } = useAuth();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [profileData, setProfileData] = useState(null);
|
const [profileData, setProfileData] = useState(null);
|
||||||
@@ -80,7 +82,8 @@ const ProfileScreen = () => {
|
|||||||
items: [
|
items: [
|
||||||
// { id: 11, title: 'Ulanmak düzgünleri', icon: 'document-text', hasArrow: true },
|
// { id: 11, title: 'Ulanmak düzgünleri', icon: 'document-text', hasArrow: true },
|
||||||
// { id: 12, title: 'Gizlinlik syýasaty', icon: 'lock-open', hasArrow: true },
|
// { id: 12, title: 'Gizlinlik syýasaty', icon: 'lock-open', hasArrow: true },
|
||||||
{ id: 13, title: 'Programma barada', icon: 'information-circle', value: 'v1.0.0', hasArrow: true },
|
{ id: 13, title: 'Programma barada', icon: 'information-circle', value: 'v1.0.0', hasArrow: false },
|
||||||
|
{ id: 14, title: 'Hasaby poz', icon: 'trash', hasArrow: false, danger: true },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -102,6 +105,12 @@ const ProfileScreen = () => {
|
|||||||
case 5: // Security
|
case 5: // Security
|
||||||
handleSecuritySettings();
|
handleSecuritySettings();
|
||||||
break;
|
break;
|
||||||
|
case 13: // About App
|
||||||
|
// Alert.alert('Üns beriň', 'Programma barada maglumatlar açylýar...');
|
||||||
|
break;
|
||||||
|
case 14: // Delete Account
|
||||||
|
handleDeleteAccount();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Alert.alert('Üns beriň', 'Bu funksiýa entek işlenok');
|
// Alert.alert('Üns beriň', 'Bu funksiýa entek işlenok');
|
||||||
}
|
}
|
||||||
@@ -201,6 +210,40 @@ const ProfileScreen = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteAccount = () => {
|
||||||
|
Alert.alert(
|
||||||
|
'Hasaby pozmak',
|
||||||
|
'Hasabyňyzy pozmak isleýändigiňize ynamlymy? Bu amal yzyna gaýtarylmaz.',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Ýok',
|
||||||
|
style: 'cancel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Hawa',
|
||||||
|
style: 'destructive',
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const result = await apiService.deleteAccount();
|
||||||
|
|
||||||
|
if (result.message == 'user deleted successfully') {
|
||||||
|
Alert.alert('Üstünlik', 'Hasabyňyz üstünlikli pozuldy.');
|
||||||
|
logout(); // Log out the user after successful deletion
|
||||||
|
} else {
|
||||||
|
Alert.alert('Ýalňyşlyk', result.error || 'Hasaby pozmak amala aşmady.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Ýalňyşlyk', error.message || 'Hasaby pozmakda näsazlyk ýüze çykdy.');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleNotificationSettings = () => {
|
const handleNotificationSettings = () => {
|
||||||
Alert.alert('Bildirişler', 'Bildiriş sazlamalary sahypasy açylýar...');
|
Alert.alert('Bildirişler', 'Bildiriş sazlamalary sahypasy açylýar...');
|
||||||
};
|
};
|
||||||
@@ -237,10 +280,10 @@ const ProfileScreen = () => {
|
|||||||
const currentUser = profileData || user;
|
const currentUser = profileData || user;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<View style={styles.container}>
|
||||||
<StatusBar style="dark" />
|
<StatusBar style="dark" />
|
||||||
|
|
||||||
<View style={styles.header}>
|
<View style={[styles.header, { paddingTop: insets.top + 16 }]}>
|
||||||
<Text style={styles.headerTitle}>Profil</Text>
|
<Text style={styles.headerTitle}>Profil</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -294,16 +337,16 @@ const ProfileScreen = () => {
|
|||||||
>
|
>
|
||||||
<View style={styles.profileItemLeft}>
|
<View style={styles.profileItemLeft}>
|
||||||
<View style={styles.profileItemIcon}>
|
<View style={styles.profileItemIcon}>
|
||||||
<Ionicons name={item.icon} size={20} color={COLORS.primary} />
|
<Ionicons name={item.icon} size={20} color={item.danger ? COLORS.error : COLORS.primary} />
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.profileItemTitle}>{item.title}</Text>
|
<Text style={item.danger ? styles.profileItemTitleDanger : styles.profileItemTitle}>{item.title}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.profileItemRight}>
|
<View style={styles.profileItemRight}>
|
||||||
{item.value && (
|
{item.value && (
|
||||||
<Text style={styles.profileItemValue}>{item.value}</Text>
|
<Text style={item.danger ? styles.profileItemValueDanger : styles.profileItemValue}>{item.value}</Text>
|
||||||
)}
|
)}
|
||||||
{item.hasArrow && (
|
{item.hasArrow && (
|
||||||
<Ionicons name="chevron-forward" size={16} color={COLORS.gray[400]} />
|
<Ionicons name="chevron-forward" size={16} color={item.danger ? COLORS.error : COLORS.gray[400]} />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -321,6 +364,21 @@ const ProfileScreen = () => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.bottomSpacing} />
|
<View style={styles.bottomSpacing} />
|
||||||
|
|
||||||
|
{/* Copyright */}
|
||||||
|
<Text style={styles.copyrightText}>
|
||||||
|
Copyright{' '}
|
||||||
|
<Text
|
||||||
|
style={styles.link}
|
||||||
|
onPress={() => Linking.openURL('https://webulgam.com')}
|
||||||
|
>
|
||||||
|
Webulgam IT Company
|
||||||
|
</Text>{' '}
|
||||||
|
© {new Date().getFullYear()}.
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.copyrightText}>
|
||||||
|
Ähli hukular goralan.
|
||||||
|
</Text>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
{/* Edit Profile Modal */}
|
{/* Edit Profile Modal */}
|
||||||
@@ -331,7 +389,7 @@ const ProfileScreen = () => {
|
|||||||
initialData={profileData || user}
|
initialData={profileData || user}
|
||||||
isLoading={isUpdatingProfile}
|
isLoading={isUpdatingProfile}
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -343,7 +401,6 @@ const styles = StyleSheet.create({
|
|||||||
header: {
|
header: {
|
||||||
backgroundColor: COLORS.white,
|
backgroundColor: COLORS.white,
|
||||||
paddingHorizontal: 24,
|
paddingHorizontal: 24,
|
||||||
paddingTop: 16,
|
|
||||||
paddingBottom: 24,
|
paddingBottom: 24,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: COLORS.gray[200],
|
borderBottomColor: COLORS.gray[200],
|
||||||
@@ -485,6 +542,24 @@ const styles = StyleSheet.create({
|
|||||||
bottomSpacing: {
|
bottomSpacing: {
|
||||||
height: 24,
|
height: 24,
|
||||||
},
|
},
|
||||||
|
copyrightText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: COLORS.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
color: COLORS.primary,
|
||||||
|
},
|
||||||
|
profileItemTitleDanger: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: COLORS.error,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
profileItemValueDanger: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: COLORS.error,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ProfileScreen;
|
export default ProfileScreen;
|
||||||
@@ -67,6 +67,37 @@ class ApiService {
|
|||||||
return authService.getTransactions(page, limit);
|
return authService.getTransactions(page, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCardTransactionsLastMonth() {
|
||||||
|
try {
|
||||||
|
const response = await authService.getCardTransactionsLastMonth();
|
||||||
|
if (response && response.data && Array.isArray(response.data.transactions)) {
|
||||||
|
const mappedTransactions = response.data.transactions.map((t, index) => ({
|
||||||
|
id: `${t.rrn}-${index}`,
|
||||||
|
type: t.sign === '-' ? 'debit' : 'credit',
|
||||||
|
description: t.binfo || t.opername,
|
||||||
|
date: `${t.operdate}`,
|
||||||
|
time: t.trantime,
|
||||||
|
amount: t.currOperSum,
|
||||||
|
currency: t.currCode,
|
||||||
|
})).reverse();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: mappedTransactions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
console.warn('Invalid transaction data structure:', response);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Could not parse transaction data.',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Transfer and payments
|
// Transfer and payments
|
||||||
async transferMoney(data) {
|
async transferMoney(data) {
|
||||||
return authService.transferMoney(data);
|
return authService.transferMoney(data);
|
||||||
|
|||||||
@@ -162,6 +162,10 @@ class AuthService {
|
|||||||
return this.makeRequest(`/user/cards/${cardId}/unblock`, null, true);
|
return this.makeRequest(`/user/cards/${cardId}/unblock`, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCardTransactionsLastMonth() {
|
||||||
|
return this.makeRequest('/card-transactions-last-month', null, true, 'GET');
|
||||||
|
}
|
||||||
|
|
||||||
async changePassword(currentPassword, newPassword) {
|
async changePassword(currentPassword, newPassword) {
|
||||||
return this.makeRequest('/user/change-password', {
|
return this.makeRequest('/user/change-password', {
|
||||||
current_password: currentPassword,
|
current_password: currentPassword,
|
||||||
@@ -170,7 +174,7 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteAccount() {
|
async deleteAccount() {
|
||||||
return this.makeRequest('/user/delete-account', null, true, 'DELETE');
|
return this.makeRequest(API_CONFIG.ENDPOINTS.AUTH.DELETE_USER, null, true, 'POST');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================================
|
// ================================
|
||||||
|
|||||||