Compare commits
44 Commits
7f49211680
...
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 | |||
| 89200eacd5 | |||
| 570a4bd335 | |||
| 4dce2a6d63 | |||
| 6044d5ae78 | |||
| 90d9a7b309 | |||
| 7ce0b92f92 | |||
| 89a1c0d9f2 | |||
| 10ac440401 | |||
| 9ee6dea980 | |||
| 23ca758917 |
5
.gitignore
vendored
@@ -35,3 +35,8 @@ yarn-error.*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
android/
|
||||
ios/
|
||||
.android/
|
||||
.ios/
|
||||
|
||||
17
App.js
@@ -2,15 +2,18 @@ import React from 'react';
|
||||
import { AuthProvider } from './src/contexts/AuthContext';
|
||||
import RootNavigator from './src/navigation/RootNavigator';
|
||||
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() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<BaseEnumsProvider>
|
||||
<StatusBar barStyle="dark-content" backgroundColor="#fff"/>
|
||||
<RootNavigator />
|
||||
</BaseEnumsProvider>
|
||||
</AuthProvider>
|
||||
<SafeAreaProvider>
|
||||
<AuthProvider>
|
||||
<BaseEnumsProvider>
|
||||
<StatusBar style="dark" />
|
||||
<RootNavigator />
|
||||
</BaseEnumsProvider>
|
||||
</AuthProvider>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}
|
||||
|
||||
17
CHANGELOG.md
@@ -0,0 +1,17 @@
|
||||
# 0.0.1
|
||||
|
||||
- Initial release
|
||||
|
||||
# 0.0.2
|
||||
|
||||
- Commented Visa/Master and Sber cards
|
||||
|
||||
```js
|
||||
{
|
||||
title: 'Halkara tölegler',
|
||||
items: [
|
||||
{ id: 9, title: 'Visa/Master tölegleri (talyplar üçin)', icon: 'logo-usd', description: 'Visa/Master' },
|
||||
{ id: 10, title: 'Sber tölegler (talyplar üçin)', icon: 'globe', description: 'Sber tölegler' },
|
||||
],
|
||||
},
|
||||
```
|
||||
34
app.json
@@ -10,20 +10,46 @@
|
||||
"splash": {
|
||||
"image": "./assets/splash-icon.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#17b69b"
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
"supportsTablet": true,
|
||||
"statusBar": {
|
||||
"barStyle": "dark-content"
|
||||
},
|
||||
"bundleIdentifier": "com.nurmuhammet.ali.tbbankonline"
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#17b69b"
|
||||
},
|
||||
"edgeToEdgeEnabled": true
|
||||
"edgeToEdgeEnabled": true,
|
||||
"package": "com.nurmuhammet.ali.tbbankonline",
|
||||
"statusBar": {
|
||||
"barStyle": "dark-content",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"eas": {
|
||||
"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 |
21
eas.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 16.14.1",
|
||||
"appVersionSource": "remote"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {
|
||||
"autoIncrement": true
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
793
package-lock.json
generated
22
package.json
@@ -4,27 +4,33 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.1.0",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
"@react-native-community/datetimepicker": "^7.7.0",
|
||||
"@react-native-async-storage/async-storage": "2.1.2",
|
||||
"@react-native-community/datetimepicker": "8.4.1",
|
||||
"@react-native-picker/picker": "^2.4.12",
|
||||
"@react-navigation/bottom-tabs": "^7.4.2",
|
||||
"@react-navigation/native": "^7.1.14",
|
||||
"@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-system-ui": "~5.0.11",
|
||||
"react": "19.0.0",
|
||||
"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-safe-area-context": "^5.5.1",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "^4.11.1",
|
||||
"react-native-svg": "^15.12.0",
|
||||
"expo-image-picker": "~16.1.4"
|
||||
"react-native-svg": "15.11.2",
|
||||
"react-native-webview": "13.13.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0"
|
||||
|
||||
@@ -724,6 +724,209 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/card-balance-quick-check": {
|
||||
"get": {
|
||||
"operationId": "cardBalance.quickCheck",
|
||||
"summary": "Quick card balance check",
|
||||
"tags": [
|
||||
"Sargytlar - Kart - Kart galyndylary"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "passport_serie",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"I-AS",
|
||||
"I-MR",
|
||||
"II-MR",
|
||||
"I-AH",
|
||||
"II-AH",
|
||||
"I-LB",
|
||||
"II-LB",
|
||||
"I-BN",
|
||||
"II-BN",
|
||||
"I-DZ",
|
||||
"II-DZ"
|
||||
]
|
||||
},
|
||||
"example": "I-AS"
|
||||
},
|
||||
{
|
||||
"name": "passport_id",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
},
|
||||
"example": 379514
|
||||
},
|
||||
{
|
||||
"name": "card_number",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "9934612100000243"
|
||||
},
|
||||
{
|
||||
"name": "card_month",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"01",
|
||||
"02",
|
||||
"03",
|
||||
"04",
|
||||
"05",
|
||||
"06",
|
||||
"07",
|
||||
"08",
|
||||
"09",
|
||||
"10",
|
||||
"11",
|
||||
"12"
|
||||
]
|
||||
},
|
||||
"example": "12"
|
||||
},
|
||||
{
|
||||
"name": "card_year",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"2024",
|
||||
"2025",
|
||||
"2026",
|
||||
"2027",
|
||||
"2028",
|
||||
"2029",
|
||||
"2030",
|
||||
"2031",
|
||||
"2032",
|
||||
"2033",
|
||||
"2034",
|
||||
"2035",
|
||||
"2036",
|
||||
"2037",
|
||||
"2038",
|
||||
"2039",
|
||||
"2040",
|
||||
"2041",
|
||||
"2042",
|
||||
"2043",
|
||||
"2044",
|
||||
"2045",
|
||||
"2046",
|
||||
"2047",
|
||||
"2048",
|
||||
"2049",
|
||||
"2050",
|
||||
"2051",
|
||||
"2052",
|
||||
"2053",
|
||||
"2054",
|
||||
"2055",
|
||||
"2056",
|
||||
"2057",
|
||||
"2058",
|
||||
"2059",
|
||||
"2060",
|
||||
"2061",
|
||||
"2062",
|
||||
"2063",
|
||||
"2064",
|
||||
"2065",
|
||||
"2066",
|
||||
"2067",
|
||||
"2068",
|
||||
"2069",
|
||||
"2070",
|
||||
"2071",
|
||||
"2072",
|
||||
"2073",
|
||||
"2074",
|
||||
"2075",
|
||||
"2076",
|
||||
"2077",
|
||||
"2078",
|
||||
"2079",
|
||||
"2080",
|
||||
"2081",
|
||||
"2082",
|
||||
"2083",
|
||||
"2084",
|
||||
"2085",
|
||||
"2086",
|
||||
"2087",
|
||||
"2088",
|
||||
"2089",
|
||||
"2090"
|
||||
]
|
||||
},
|
||||
"example": "2049"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"data": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"message",
|
||||
"url"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/AuthenticationException"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#/components/responses/ValidationException"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/card-balances": {
|
||||
"get": {
|
||||
"operationId": "cardBalance.index",
|
||||
@@ -6795,6 +6998,9 @@
|
||||
},
|
||||
"card_year": {
|
||||
"type": "string"
|
||||
},
|
||||
"card_name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -6804,7 +7010,8 @@
|
||||
"passport_id",
|
||||
"card_number",
|
||||
"card_month",
|
||||
"card_year"
|
||||
"card_year",
|
||||
"card_name"
|
||||
],
|
||||
"title": "ProfileResponse"
|
||||
},
|
||||
@@ -6947,6 +7154,11 @@
|
||||
"2090"
|
||||
],
|
||||
"example": "2049"
|
||||
},
|
||||
"card_name": {
|
||||
"type": "string",
|
||||
"example": "Nurmuhammet Allanov",
|
||||
"maxLength": 255
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
21
roadmap.md
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"build": {
|
||||
"preview": {
|
||||
"android": {
|
||||
"buildType": "apk"
|
||||
}
|
||||
},
|
||||
"preview2": {
|
||||
"android": {
|
||||
"gradleCommand": ":app:assembleRelease"
|
||||
}
|
||||
},
|
||||
"preview3": {
|
||||
"developmentClient": true
|
||||
},
|
||||
"preview4": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,8 @@ const EditProfileModal = ({
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
const [showPassportPicker, setShowPassportPicker] = useState(false);
|
||||
const [showMonthPicker, setShowMonthPicker] = useState(false);
|
||||
const [showYearPicker, setShowYearPicker] = useState(false);
|
||||
|
||||
const phoneInputRef = useRef(null);
|
||||
const passwordInputRef = useRef(null);
|
||||
@@ -88,9 +90,9 @@ const EditProfileModal = ({
|
||||
newErrors.card_name = 'Kartyň ady 255 harpdan köp bolmaly däl';
|
||||
}
|
||||
|
||||
// Card number validation (optional, must be 16 digits)
|
||||
if (formData.card_number && !/^\d{16}$/.test(formData.card_number.trim())) {
|
||||
newErrors.card_number = 'Kart belgisi 16 sany sandan durmaly';
|
||||
// Card number validation (optional, must be 16 digits and allow hyphens)
|
||||
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 (mysal: 9999-9999-9999-9999)';
|
||||
}
|
||||
|
||||
// Card month validation (optional, 1-12)
|
||||
@@ -102,9 +104,9 @@ const EditProfileModal = ({
|
||||
}
|
||||
|
||||
// Card year validation (optional, 4 digits and reasonable year)
|
||||
if (formData.card_year && !/^\d{4}$/.test(formData.card_year.trim())) {
|
||||
newErrors.card_year = 'Ýyl 4 sany sandan durmaly';
|
||||
}
|
||||
// if (formData.card_year) {
|
||||
|
||||
// }
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
@@ -147,7 +149,8 @@ const EditProfileModal = ({
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -167,6 +170,7 @@ const EditProfileModal = ({
|
||||
card_number: initialData.card_number ? initialData.card_number.toString() : '',
|
||||
});
|
||||
setErrors({});
|
||||
Keyboard.dismiss(); // Dismiss keyboard when modal closes
|
||||
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 (
|
||||
<Modal
|
||||
visible={visible}
|
||||
@@ -256,167 +383,193 @@ const EditProfileModal = ({
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
<View style={styles.content}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={handleClose} style={styles.closeButton}>
|
||||
<Ionicons name="close" size={24} color={COLORS.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.title}>Şahsy maglumatlar</Text>
|
||||
<View style={styles.placeholder} />
|
||||
</View>
|
||||
|
||||
{/* Form */}
|
||||
<ScrollView style={styles.form} showsVerticalScrollIndicator={false}>
|
||||
<View style={styles.formSection}>
|
||||
<Text style={styles.sectionTitle}>Esasy maglumatlar</Text>
|
||||
|
||||
<Input
|
||||
label="Ady *"
|
||||
value={formData.name}
|
||||
onChangeText={(value) => updateFormData('name', value)}
|
||||
error={errors.name}
|
||||
maxLength={255}
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => phoneInputRef.current?.focus()}
|
||||
/>
|
||||
|
||||
<Input
|
||||
ref={phoneInputRef}
|
||||
label="Telefon belgisi *"
|
||||
value={formData.phone}
|
||||
onChangeText={(value) => updateFormData('phone', value)}
|
||||
error={errors.phone}
|
||||
keyboardType="numeric"
|
||||
maxLength={8}
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => passwordInputRef.current?.focus()}
|
||||
/>
|
||||
|
||||
<Input
|
||||
ref={passwordInputRef}
|
||||
label="Täze parol"
|
||||
value={formData.password}
|
||||
onChangeText={(value) => updateFormData('password', value)}
|
||||
error={errors.password}
|
||||
secureTextEntry
|
||||
placeholder="Parol üýtgetmezlik üçin boş goýuň"
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => passportIdInputRef.current?.focus()}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.formSection}>
|
||||
<Text style={styles.sectionTitle}>Passport maglumatlary</Text>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.label}>Passport seriýasy</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.pickerButton, errors.passport_serie && styles.inputError]}
|
||||
onPress={() => setShowPassportPicker(true)}
|
||||
>
|
||||
<Text style={[styles.pickerButtonText, !formData.passport_serie && styles.placeholderText]}>
|
||||
{formData.passport_serie || 'Saýlaň'}
|
||||
</Text>
|
||||
<Ionicons name="chevron-down" size={20} color={COLORS.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
{errors.passport_serie && (
|
||||
<Text style={styles.errorText}>{errors.passport_serie}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Input
|
||||
ref={passportIdInputRef}
|
||||
label="Passport ID"
|
||||
value={formData.passport_id}
|
||||
onChangeText={(value) => updateFormData('passport_id', value)}
|
||||
error={errors.passport_id}
|
||||
keyboardType="numeric"
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => cardNameInputRef.current?.focus()}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.formSection}>
|
||||
<Text style={styles.sectionTitle}>Kart maglumatlary</Text>
|
||||
|
||||
<Input
|
||||
ref={cardNameInputRef}
|
||||
label="Kartdaky ady"
|
||||
value={formData.card_name}
|
||||
onChangeText={(value) => updateFormData('card_name', value)}
|
||||
error={errors.card_name}
|
||||
maxLength={255}
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => cardNumberInputRef.current?.focus()}
|
||||
/>
|
||||
|
||||
<Input
|
||||
ref={cardNumberInputRef}
|
||||
label="Kart belgisi"
|
||||
value={formData.card_number}
|
||||
onChangeText={(value) => updateFormData('card_number', value)}
|
||||
error={errors.card_number}
|
||||
keyboardType="numeric"
|
||||
maxLength={16}
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => cardMonthInputRef.current?.focus()}
|
||||
/>
|
||||
|
||||
<Input
|
||||
ref={cardMonthInputRef}
|
||||
label="Kart aýy (MM)"
|
||||
value={formData.card_month}
|
||||
onChangeText={(value) => updateFormData('card_month', value)}
|
||||
error={errors.card_month}
|
||||
keyboardType="numeric"
|
||||
maxLength={2}
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => cardYearInputRef.current?.focus()}
|
||||
/>
|
||||
|
||||
<Input
|
||||
ref={cardYearInputRef}
|
||||
label="Kartyň senesi (YYYY)"
|
||||
value={formData.card_year}
|
||||
onChangeText={(value) => updateFormData('card_year', value)}
|
||||
error={errors.card_year}
|
||||
keyboardType="numeric"
|
||||
maxLength={4}
|
||||
returnKeyType="done"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.note}>
|
||||
<Ionicons name="information-circle" size={16} color={COLORS.textSecondary} />
|
||||
<Text style={styles.noteText}>
|
||||
* belgisi bolan meýdanlar hökmany doldurulmaly
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* Save Button */}
|
||||
<View style={styles.footer}>
|
||||
<Button
|
||||
title="Ýatda sakla"
|
||||
onPress={handleSave}
|
||||
disabled={isLoading}
|
||||
style={styles.saveButton}
|
||||
/>
|
||||
{isLoading && (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={COLORS.primary}
|
||||
style={styles.loader}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.content}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={handleClose} style={styles.closeButton}>
|
||||
<Ionicons name="close" size={24} color={COLORS.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.title}>Şahsy maglumatlar</Text>
|
||||
<View style={styles.placeholder} />
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
|
||||
{/* Form */}
|
||||
<ScrollView
|
||||
style={styles.form}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
contentContainerStyle={{paddingBottom:80}}
|
||||
keyboardDismissMode="on-drag"
|
||||
>
|
||||
<View style={styles.formSection}>
|
||||
<Text style={styles.sectionTitle}>Esasy maglumatlar</Text>
|
||||
|
||||
<Input
|
||||
label="Ady *"
|
||||
value={formData.name}
|
||||
onChangeText={(value) => updateFormData('name', value)}
|
||||
error={errors.name}
|
||||
maxLength={255}
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => phoneInputRef.current?.focus()}
|
||||
/>
|
||||
|
||||
<Input
|
||||
ref={phoneInputRef}
|
||||
label="Telefon belgisi *"
|
||||
value={formData.phone}
|
||||
onChangeText={(value) => updateFormData('phone', value)}
|
||||
error={errors.phone}
|
||||
keyboardType="numeric"
|
||||
maxLength={8}
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => passwordInputRef.current?.focus()}
|
||||
/>
|
||||
|
||||
<Input
|
||||
ref={passwordInputRef}
|
||||
label="Täze parol"
|
||||
value={formData.password}
|
||||
onChangeText={(value) => updateFormData('password', value)}
|
||||
error={errors.password}
|
||||
secureTextEntry
|
||||
placeholder="Parol üýtgetmezlik üçin boş goýuň"
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => passportIdInputRef.current?.focus()}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.formSection}>
|
||||
<Text style={styles.sectionTitle}>Passport maglumatlary</Text>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.label}>Passport seriýasy</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.pickerButton, errors.passport_serie && styles.inputError]}
|
||||
onPress={() => setShowPassportPicker(true)}
|
||||
>
|
||||
<Text style={[styles.pickerButtonText, !formData.passport_serie && styles.placeholderText]}>
|
||||
{formData.passport_serie || 'Saýlaň'}
|
||||
</Text>
|
||||
<Ionicons name="chevron-down" size={20} color={COLORS.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
{errors.passport_serie && (
|
||||
<Text style={styles.errorText}>{errors.passport_serie}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Input
|
||||
ref={passportIdInputRef}
|
||||
label="Passport ID"
|
||||
value={formData.passport_id}
|
||||
onChangeText={(value) => updateFormData('passport_id', value)}
|
||||
error={errors.passport_id}
|
||||
keyboardType="numeric"
|
||||
returnKeyType="next"
|
||||
maxLength={6}
|
||||
onSubmitEditing={() => cardNameInputRef.current?.focus()}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.formSection}>
|
||||
<Text style={styles.sectionTitle}>Kart maglumatlary</Text>
|
||||
|
||||
<Input
|
||||
ref={cardNameInputRef}
|
||||
label="Kartdaky ady"
|
||||
value={formData.card_name}
|
||||
onChangeText={(value) => updateFormData('card_name', value)}
|
||||
error={errors.card_name}
|
||||
maxLength={255}
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => cardNumberInputRef.current?.focus()}
|
||||
/>
|
||||
|
||||
<Input
|
||||
ref={cardNumberInputRef}
|
||||
label="Kart belgisi"
|
||||
value={formData.card_number}
|
||||
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}
|
||||
keyboardType="numeric"
|
||||
maxLength={19} // 16 digits + 3 hyphens
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => cardMonthInputRef.current?.focus()}
|
||||
/>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.label}>Kart aýy (MM)</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.pickerButton, errors.card_month && styles.inputError]}
|
||||
onPress={() => setShowMonthPicker(true)}
|
||||
>
|
||||
<Text style={[styles.pickerButtonText, !formData.card_month && styles.placeholderText]}>
|
||||
{formData.card_month || 'Saýlaň'}
|
||||
</Text>
|
||||
<Ionicons name="chevron-down" size={20} color={COLORS.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
{errors.card_month && (
|
||||
<Text style={styles.errorText}>{errors.card_month}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.label}>Kartyň senesi (YYYY)</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.pickerButton, errors.card_year && styles.inputError]}
|
||||
onPress={() => setShowYearPicker(true)}
|
||||
>
|
||||
<Text style={[styles.pickerButtonText, !formData.card_year && styles.placeholderText]}>
|
||||
{formData.card_year || 'Saýlaň'}
|
||||
</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 style={styles.note}>
|
||||
<Ionicons name="information-circle" size={16} color={COLORS.textSecondary} />
|
||||
<Text style={styles.noteText}>
|
||||
* belgisi bolan meýdanlar hökmany doldurulmaly
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* Save Button */}
|
||||
<View style={styles.footer}>
|
||||
<Button
|
||||
title="Ýatda sakla"
|
||||
onPress={handleSave}
|
||||
disabled={isLoading}
|
||||
style={styles.saveButton}
|
||||
/>
|
||||
{isLoading && (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={COLORS.primary}
|
||||
style={styles.loader}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
{renderPassportSeriesPicker()}
|
||||
{renderMonthPicker()}
|
||||
{renderYearPicker()}
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
);
|
||||
@@ -451,7 +604,6 @@ const styles = StyleSheet.create({
|
||||
width: 32,
|
||||
},
|
||||
form: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
formSection: {
|
||||
|
||||
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;
|
||||
@@ -6,6 +6,8 @@ const BaseEnumsContext = createContext({ enums: null, refresh: () => {}, getEnum
|
||||
export const BaseEnumsProvider = ({ children }) => {
|
||||
const [enums, setEnums] = useState(null);
|
||||
const [lastFetched, setLastFetched] = useState(0);
|
||||
const [branchesByRegion, setBranchesByRegion] = useState({});
|
||||
const [branchesFetchedAt, setBranchesFetchedAt] = useState(0);
|
||||
|
||||
const fetchEnums = async () => {
|
||||
try {
|
||||
@@ -18,11 +20,24 @@ export const BaseEnumsProvider = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBranches = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_CONFIG.BASE_URL}/branches?groupBy=region`);
|
||||
const json = await res.json();
|
||||
setBranchesByRegion(json || {});
|
||||
setBranchesFetchedAt(Date.now());
|
||||
} catch (e) {
|
||||
console.warn('Failed to fetch branches', e.message);
|
||||
}
|
||||
};
|
||||
|
||||
// initial fetch and 60s refresh
|
||||
useEffect(() => {
|
||||
fetchEnums();
|
||||
fetchBranches();
|
||||
const id = setInterval(fetchEnums, 60000);
|
||||
return () => clearInterval(id);
|
||||
const idB = setInterval(fetchBranches, 60000);
|
||||
return () => { clearInterval(id); clearInterval(idB);} ;
|
||||
}, []);
|
||||
|
||||
const getEnums = async () => {
|
||||
@@ -42,8 +57,15 @@ export const BaseEnumsProvider = ({ children }) => {
|
||||
return Object.entries(enums[category]).map(([value, label]) => ({ value, label }));
|
||||
};
|
||||
|
||||
const getBranches = async (regionKey) => {
|
||||
if (Date.now() - branchesFetchedAt > 60000) {
|
||||
await fetchBranches();
|
||||
}
|
||||
return regionKey ? branchesByRegion[regionKey] || [] : [];
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseEnumsContext.Provider value={{ enums, refresh: fetchEnums, getEnums, getLabel, getOptions }}>
|
||||
<BaseEnumsContext.Provider value={{ enums, refresh: fetchEnums, getEnums, getLabel, getOptions, getBranches }}>
|
||||
{children}
|
||||
</BaseEnumsContext.Provider>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,8 @@ import React from 'react';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
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 MenuNavigator from './MenuNavigator';
|
||||
@@ -10,6 +12,8 @@ import ProfileScreen from '../screens/Main/ProfileScreen';
|
||||
const Tab = createBottomTabNavigator();
|
||||
|
||||
const MainNavigator = () => {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={({ route }) => ({
|
||||
@@ -33,9 +37,17 @@ const MainNavigator = () => {
|
||||
backgroundColor: COLORS.white,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: COLORS.gray[200],
|
||||
paddingBottom: 8,
|
||||
paddingBottom: (insets.bottom || 16),
|
||||
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: {
|
||||
fontSize: 12,
|
||||
@@ -43,7 +55,7 @@ const MainNavigator = () => {
|
||||
marginTop: 4,
|
||||
},
|
||||
tabBarItemStyle: {
|
||||
paddingVertical: 4,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import MenuScreen from '../screens/Main/MenuScreen';
|
||||
import LoanRemainingOrdersScreen from '../screens/Loan/LoanRemainingOrdersScreen';
|
||||
import CreateLoanRemainingOrderScreen from '../screens/Loan/CreateLoanRemainingOrderScreen';
|
||||
@@ -12,6 +13,18 @@ import LoanOrderDetailsScreen from '../screens/Loan/LoanOrderDetailsScreen';
|
||||
import CardTransactionOrdersScreen from '../screens/Card/CardTransactionOrdersScreen';
|
||||
import CreateCardTransactionOrderScreen from '../screens/Card/CreateCardTransactionOrderScreen';
|
||||
import CardTransactionOrderDetailsScreen from '../screens/Card/CardTransactionOrderDetailsScreen';
|
||||
import CardBalanceOrdersScreen from '../screens/Card/CardBalanceOrdersScreen';
|
||||
import CreateCardBalanceOrderScreen from '../screens/Card/CreateCardBalanceOrderScreen';
|
||||
import CardBalanceOrderDetailsScreen from '../screens/Card/CardBalanceOrderDetailsScreen';
|
||||
import CardRequisiteOrdersScreen from '../screens/Card/CardRequisiteOrdersScreen';
|
||||
import CreateCardRequisiteOrderScreen from '../screens/Card/CreateCardRequisiteOrderScreen';
|
||||
import CardRequisiteOrderDetailsScreen from '../screens/Card/CardRequisiteOrderDetailsScreen';
|
||||
import CardPinOrdersScreen from '../screens/Card/CardPinOrdersScreen';
|
||||
import CreateCardPinOrderScreen from '../screens/Card/CreateCardPinOrderScreen';
|
||||
import CardPinOrderDetailsScreen from '../screens/Card/CardPinOrderDetailsScreen';
|
||||
import CardOrdersScreen from '../screens/Card/CardOrdersScreen';
|
||||
import CreateCardOrderScreen from '../screens/Card/CreateCardOrderScreen';
|
||||
import CardOrderDetailsScreen from '../screens/Card/CardOrderDetailsScreen';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
@@ -29,6 +42,18 @@ const MenuNavigator = () => (
|
||||
<Stack.Screen name="CardTransactionOrders" component={CardTransactionOrdersScreen} />
|
||||
<Stack.Screen name="CreateCardTransactionOrder" component={CreateCardTransactionOrderScreen} />
|
||||
<Stack.Screen name="CardTransactionOrderDetails" component={CardTransactionOrderDetailsScreen} />
|
||||
<Stack.Screen name="CardBalanceOrders" component={CardBalanceOrdersScreen} />
|
||||
<Stack.Screen name="CreateCardBalanceOrder" component={CreateCardBalanceOrderScreen} />
|
||||
<Stack.Screen name="CardBalanceOrderDetails" component={CardBalanceOrderDetailsScreen} />
|
||||
<Stack.Screen name="CardRequisiteOrders" component={CardRequisiteOrdersScreen} />
|
||||
<Stack.Screen name="CreateCardRequisiteOrder" component={CreateCardRequisiteOrderScreen} />
|
||||
<Stack.Screen name="CardRequisiteOrderDetails" component={CardRequisiteOrderDetailsScreen} />
|
||||
<Stack.Screen name="CardPinOrders" component={CardPinOrdersScreen} />
|
||||
<Stack.Screen name="CreateCardPinOrder" component={CreateCardPinOrderScreen} />
|
||||
<Stack.Screen name="CardPinOrderDetails" component={CardPinOrderDetailsScreen} />
|
||||
<Stack.Screen name="CardOrders" component={CardOrdersScreen} />
|
||||
<Stack.Screen name="CreateCardOrder" component={CreateCardOrderScreen} />
|
||||
<Stack.Screen name="CardOrderDetails" component={CardOrderDetailsScreen} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
SafeAreaView,
|
||||
KeyboardAvoidingView,
|
||||
ScrollView,
|
||||
Platform,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
TouchableWithoutFeedback,
|
||||
Keyboard,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import Button from '../../components/Button';
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
SafeAreaView,
|
||||
KeyboardAvoidingView,
|
||||
ScrollView,
|
||||
Platform,
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
TouchableWithoutFeedback,
|
||||
Keyboard,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import Button from '../../components/Button';
|
||||
|
||||
@@ -3,27 +3,40 @@ import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
SafeAreaView,
|
||||
Alert,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
Keyboard,
|
||||
BackHandler,
|
||||
Platform,
|
||||
KeyboardAvoidingView,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
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';
|
||||
|
||||
const CELL_COUNT = 6;
|
||||
|
||||
const VerificationScreen = ({ navigation }) => {
|
||||
const [code, setCode] = useState('');
|
||||
const [countdown, setCountdown] = useState(60);
|
||||
const [canResend, setCanResend] = useState(false);
|
||||
const { verify, isLoading, pendingVerification, clearPendingVerification } = useAuth();
|
||||
const countdownRef = useRef(null);
|
||||
const ref = useBlurOnFulfill({ value: code, cellCount: CELL_COUNT });
|
||||
const [props, getCellOnLayoutHandler] = useClearByFocusCell({
|
||||
value: code,
|
||||
setValue: setCode,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingVerification) {
|
||||
@@ -49,8 +62,8 @@ const VerificationScreen = ({ navigation }) => {
|
||||
};
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
BackHandler.addEventListener('hardwareBackPress', onBackPress);
|
||||
return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress);
|
||||
const sub = BackHandler.addEventListener('hardwareBackPress', onBackPress);
|
||||
return () => sub.remove();
|
||||
}
|
||||
}, [handleGoBack])
|
||||
);
|
||||
@@ -73,18 +86,18 @@ const VerificationScreen = ({ navigation }) => {
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleVerify = async () => {
|
||||
if (!code.trim()) {
|
||||
const handleVerify = async (filledCode) => {
|
||||
if (!filledCode.trim()) {
|
||||
Alert.alert('Ýalňyşlyk', 'Tassyklama koduny giriziň');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^\d{6}$/.test(code.trim())) {
|
||||
if (!/^\d{6}$/.test(filledCode.trim())) {
|
||||
Alert.alert('Ýalňyşlyk', 'Tassyklama kody 6 sanly bolmaly');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await verify(code.trim());
|
||||
const result = await verify(filledCode.trim());
|
||||
|
||||
if (result.success) {
|
||||
// Navigation will be handled by AuthContext
|
||||
@@ -93,6 +106,8 @@ const VerificationScreen = ({ navigation }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyWrapper = () => handleVerify(code);
|
||||
|
||||
const handleResendCode = async () => {
|
||||
if (!canResend) return;
|
||||
|
||||
@@ -134,54 +149,66 @@ const VerificationScreen = ({ navigation }) => {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.logoContainer}>
|
||||
<View style={styles.verificationIcon}>
|
||||
<Text style={styles.verificationIconText}>✓</Text>
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.logoContainer}>
|
||||
<View style={styles.verificationIcon}>
|
||||
<Text style={styles.verificationIconText}>✓</Text>
|
||||
</View>
|
||||
<Text style={styles.title}>Tassyklama</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
{formatPhoneNumber(pendingVerification.phone)} belgisine iberilen
|
||||
6 sanly tassyklama koduny giriziň
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.title}>Tassyklama</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
{formatPhoneNumber(pendingVerification.phone)} belgisine iberilen
|
||||
6 sanly tassyklama koduny giriziň
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.formContainer}>
|
||||
<Input
|
||||
label="Tassyklama kody"
|
||||
value={code}
|
||||
onChangeText={setCode}
|
||||
placeholder="123456"
|
||||
keyboardType="numeric"
|
||||
maxLength={6}
|
||||
style={styles.codeInput}
|
||||
textAlign="center"
|
||||
returnKeyType="done"
|
||||
onSubmitEditing={handleVerify}
|
||||
/>
|
||||
<View style={styles.formContainer}>
|
||||
<CodeField
|
||||
ref={ref}
|
||||
{...props}
|
||||
value={code}
|
||||
onChangeText={setCode}
|
||||
cellCount={CELL_COUNT}
|
||||
rootStyle={styles.otpContainer}
|
||||
keyboardType="number-pad"
|
||||
textContentType="oneTimeCode"
|
||||
renderCell={({ index, symbol, isFocused }) => (
|
||||
<Text
|
||||
key={index}
|
||||
style={[styles.otpInput, isFocused && styles.otpInputHighlight]}
|
||||
onLayout={getCellOnLayoutHandler(index)}>
|
||||
{symbol || (isFocused ? <Cursor/> : null)}
|
||||
</Text>
|
||||
)}
|
||||
onSubmitEditing={handleVerifyWrapper}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title="Tassykla"
|
||||
onPress={handleVerify}
|
||||
loading={isLoading}
|
||||
style={styles.verifyButton}
|
||||
/>
|
||||
<Button
|
||||
title="Tassykla"
|
||||
onPress={handleVerifyWrapper}
|
||||
loading={isLoading}
|
||||
style={styles.verifyButton}
|
||||
/>
|
||||
|
||||
<View style={styles.resendContainer}>
|
||||
{canResend ? (
|
||||
<TouchableOpacity onPress={handleResendCode}>
|
||||
<Text style={styles.resendText}>Kodu gaýtadan iber</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<Text style={styles.countdownText}>
|
||||
Gaýtadan ibermek üçin {countdown} sekunt garaşyň
|
||||
</Text>
|
||||
)}
|
||||
<View style={styles.resendContainer}>
|
||||
{canResend ? (
|
||||
<TouchableOpacity onPress={handleResendCode}>
|
||||
<Text style={styles.resendText}>Kodu gaýtadan iber</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<Text style={styles.countdownText}>
|
||||
Gaýtadan ibermek üçin {countdown} sekunt garaşyň
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</TouchableWithoutFeedback>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
@@ -251,10 +278,25 @@ const styles = StyleSheet.create({
|
||||
formContainer: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
codeInput: {
|
||||
width: '100%',
|
||||
otpContainer: {
|
||||
width: '90%',
|
||||
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: {
|
||||
width: '100%',
|
||||
marginBottom: 32,
|
||||
|
||||
315
src/screens/Card/CardBalanceOrderDetailsScreen.js
Normal file
@@ -0,0 +1,315 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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 { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import apiService from '../../services/apiService';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import DateInput from '../../components/DateInput';
|
||||
import { Linking } from 'react-native';
|
||||
|
||||
const DetailRow = ({ label, value, showBorder = true }) => (
|
||||
<View style={[styles.detailRow, showBorder && styles.detailRowBorder]}>
|
||||
<Text style={styles.detailKey}>{label}</Text>
|
||||
<Text style={styles.detailValue}>{String(value)}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const CardBalanceOrderDetailsScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const route = useRoute();
|
||||
const { orderId } = route.params || {};
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [order, setOrder] = useState(null);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [loadingCardInfo, setLoadingCardInfo] = useState(false);
|
||||
const [cardInfo, setCardInfo] = useState(null);
|
||||
|
||||
const fetchDetails = async () => {
|
||||
setLoading(true);
|
||||
const res = await apiService.getCardBalanceOrder(orderId);
|
||||
if (res.success) {
|
||||
setOrder(res.data);
|
||||
} else {
|
||||
Alert.alert('Error', res.error || 'Could not fetch details');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDetails();
|
||||
}, []);
|
||||
|
||||
const handleDelete = () => {
|
||||
Alert.alert('Confirm', 'Are you sure you want to delete this order?', [
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{ text: 'Delete', style: 'destructive', onPress: deleteOrder },
|
||||
]);
|
||||
};
|
||||
|
||||
const deleteOrder = async () => {
|
||||
const res = await apiService.deleteCardBalanceOrder(orderId);
|
||||
if (res.success) {
|
||||
Alert.alert('Deleted', res.message || 'Order deleted', [
|
||||
{ text: 'OK', onPress: () => navigation.goBack() },
|
||||
]);
|
||||
} else {
|
||||
Alert.alert('Error', res.error || 'Could not delete');
|
||||
}
|
||||
};
|
||||
|
||||
const openViewModal = async () => {
|
||||
setModalVisible(true);
|
||||
setLoadingCardInfo(true);
|
||||
// Fetch card info via download endpoint
|
||||
const res = await apiService.downloadCardBalances(orderId);
|
||||
if (res.success && res.data?.status && res.data.data) {
|
||||
setCardInfo(res.data.data);
|
||||
} else {
|
||||
Alert.alert('Ýalňyşlyk', res.error || 'Maglumat tapylmady');
|
||||
}
|
||||
setLoadingCardInfo(false);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator size="large" color={COLORS.primary} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!order) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<Text>No data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar style="dark" />
|
||||
<TouchableOpacity style={styles.backBtn} onPress={() => navigation.goBack()}>
|
||||
<Ionicons name="close" size={28} color={COLORS.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<ScrollView contentContainerStyle={{ paddingBottom: 40, paddingHorizontal: 24 }}>
|
||||
<Text style={styles.title}>Kart galyndylary</Text>
|
||||
|
||||
<View style={styles.detailCard}>
|
||||
<DetailRow label="ID" value={order.id} />
|
||||
{order.card_number && <DetailRow label="Kart" value={order.card_number} />}
|
||||
{order.card_month && order.card_year && <DetailRow label="Kartyň möhleti" value={`${order.card_month}/${order.card_year}`} />}
|
||||
{order.passport_serie && order.passport_id && <DetailRow label="Pasport" value={`${order.passport_serie} ${order.passport_id}`} showBorder={false} />}
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={openViewModal}>
|
||||
<Text style={styles.actionText}>Görmek</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.deleteBtn} onPress={handleDelete}>
|
||||
<Text style={styles.deleteText}>Poz</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
|
||||
{/* View modal */}
|
||||
<Modal visible={modalVisible} transparent animationType="fade" onRequestClose={() => setModalVisible(false)}>
|
||||
<View style={styles.modalBackdrop}>
|
||||
<View style={[styles.modalCard, { maxHeight: '80%' }] }>
|
||||
{loadingCardInfo ? (
|
||||
<ActivityIndicator size="large" color={COLORS.primary} />
|
||||
) : cardInfo ? (
|
||||
<View>
|
||||
<Text style={styles.bankName}>{cardInfo.depName || 'Türkmenistanyň "Türkmenbaşy" paýdarlar täjirçilik banky'}</Text>
|
||||
<Text style={styles.cardType}>{cardInfo.cardName || 'Kart'}</Text>
|
||||
<Text style={styles.cardLabel}>KART BELGISI</Text>
|
||||
|
||||
<Text style={styles.cardNumber}>{`${order.card_number?.slice(0,6)}******${order.card_number?.slice(-4)}`}</Text>
|
||||
|
||||
<View style={{ flexDirection: 'row', marginTop: 24 }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.subLabel}>KARTYŇ EÝESINIŇ ADY</Text>
|
||||
<Text style={styles.subValue}>{cardInfo.clientName || '-'}</Text>
|
||||
</View>
|
||||
<View style={{ width: 100 }}>
|
||||
<Text style={styles.subLabel}>MÖHLETI</Text>
|
||||
<Text style={styles.subValue}>{order.card_month}/{order.card_year}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{cardInfo.balance !== undefined && (
|
||||
<View style={styles.balanceBox}>
|
||||
<Text style={styles.balanceLabel}>GALYNDY</Text>
|
||||
<Text style={styles.balanceValue}>{cardInfo.balance} {cardInfo.valCode || 'TMT'}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<Text>Maglumat ýok</Text>
|
||||
)}
|
||||
|
||||
<TouchableOpacity onPress={() => setModalVisible(false)} style={{ marginTop: 24, alignItems: 'center' }}>
|
||||
<Text style={{ color: COLORS.error, fontWeight: '600' }}>Ýap</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: COLORS.backgroundSecondary,
|
||||
paddingTop: 40,
|
||||
},
|
||||
centered: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
backBtn: {
|
||||
alignSelf: 'flex-end',
|
||||
marginRight: 24,
|
||||
marginBottom: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: COLORS.textPrimary,
|
||||
marginBottom: 24,
|
||||
},
|
||||
detailCard: {
|
||||
backgroundColor: COLORS.white,
|
||||
borderRadius: 12,
|
||||
padding: 20,
|
||||
marginBottom: 32,
|
||||
},
|
||||
detailRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 12,
|
||||
},
|
||||
detailRowBorder: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.border,
|
||||
},
|
||||
detailKey: {
|
||||
fontWeight: '600',
|
||||
color: COLORS.textSecondary,
|
||||
},
|
||||
detailValue: {
|
||||
color: COLORS.textPrimary,
|
||||
},
|
||||
actionBtn: {
|
||||
backgroundColor: COLORS.primary,
|
||||
paddingVertical: 16,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
actionText: {
|
||||
color: COLORS.white,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
deleteBtn: {
|
||||
backgroundColor: COLORS.error,
|
||||
paddingVertical: 16,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
deleteText: {
|
||||
color: COLORS.white,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
modalBackdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.25)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
modalCard: {
|
||||
backgroundColor: COLORS.white,
|
||||
borderRadius: 12,
|
||||
width: '100%',
|
||||
padding: 24,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: COLORS.textPrimary,
|
||||
marginBottom: 16,
|
||||
},
|
||||
submitBtn: {
|
||||
marginTop: 8,
|
||||
backgroundColor: COLORS.primary,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
submitText: {
|
||||
color: COLORS.white,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
bankName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: COLORS.textPrimary,
|
||||
marginBottom: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
cardType: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: COLORS.textSecondary,
|
||||
marginBottom: 22,
|
||||
textAlign: 'center',
|
||||
},
|
||||
cardLabel: {
|
||||
fontSize: 12,
|
||||
color: COLORS.textSecondary,
|
||||
marginTop: 4,
|
||||
},
|
||||
cardNumber: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
marginTop: 4,
|
||||
},
|
||||
subLabel: {
|
||||
fontSize: 12,
|
||||
color: COLORS.textSecondary,
|
||||
},
|
||||
subValue: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: COLORS.textPrimary,
|
||||
marginTop: 4,
|
||||
},
|
||||
balanceBox: {
|
||||
borderWidth: 1,
|
||||
borderColor: COLORS.gray[300],
|
||||
borderRadius: 4,
|
||||
padding: 12,
|
||||
marginTop: 24,
|
||||
},
|
||||
balanceLabel: {
|
||||
fontSize: 12,
|
||||
color: COLORS.textSecondary,
|
||||
marginBottom: 4,
|
||||
},
|
||||
balanceValue: {
|
||||
fontSize: 22,
|
||||
fontWeight: '700',
|
||||
color: COLORS.success || '#2ecc71',
|
||||
},
|
||||
});
|
||||
|
||||
export default CardBalanceOrderDetailsScreen;
|
||||
196
src/screens/Card/CardBalanceOrdersScreen.js
Normal file
@@ -0,0 +1,196 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
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 { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import apiService from '../../services/apiService';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
|
||||
const CARD_BG = '#F1F9F1';
|
||||
const CIRCLE_BG = '#A2E4A4';
|
||||
|
||||
const formatCardNumber = (num) => {
|
||||
if (!num) return '';
|
||||
return num.replace(/\s+/g, '').replace(/(.{4})/g, '$1 ').trim();
|
||||
};
|
||||
|
||||
const CardBalanceOrdersScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const [orders, setOrders] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const fetchOrders = async () => {
|
||||
try {
|
||||
const res = await apiService.getCardBalanceOrders();
|
||||
if (res.success) {
|
||||
setOrders(Array.isArray(res.data) ? res.data : []);
|
||||
} else {
|
||||
console.warn(res.error);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fetchOrders();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const onRefresh = () => {
|
||||
setRefreshing(true);
|
||||
fetchOrders();
|
||||
};
|
||||
|
||||
const handleItemPress = (item) => {
|
||||
navigation.navigate('CardBalanceOrderDetails', { orderId: item.id });
|
||||
};
|
||||
|
||||
const renderItem = ({ item }) => {
|
||||
const cardNum = item.card_number || '';
|
||||
const masked = cardNum ? `${cardNum.slice(0, 6)}******${cardNum.slice(-4)}` : '';
|
||||
const created = item.created_at ? new Date(item.created_at).toLocaleDateString() : '';
|
||||
const passportLine = item.passport_serie && item.passport_id ? `Pasport: ${item.passport_serie} ${item.passport_id}` : '';
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={styles.card} onPress={() => handleItemPress(item)}>
|
||||
<View style={styles.circle}>
|
||||
<Text style={styles.circleText}>{item.id}</Text>
|
||||
</View>
|
||||
<View style={styles.cardContent}>
|
||||
<Text style={styles.cardNumber}>{masked}</Text>
|
||||
{passportLine !== '' && <Text style={styles.passportText}>{passportLine}</Text>}
|
||||
<Text style={styles.dateText}>{created}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator size="large" color={COLORS.primary} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar style="dark" />
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => navigation.goBack()} style={{ paddingRight: 12 }}>
|
||||
<Ionicons name="arrow-back" size={24} color={COLORS.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>Kart galyndylary</Text>
|
||||
</View>
|
||||
<FlatList
|
||||
data={orders}
|
||||
keyExtractor={(item) => item.id?.toString()}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={orders.length === 0 && styles.emptyContainer}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
||||
ListEmptyComponent={<Text style={styles.emptyText}>No orders yet</Text>}
|
||||
/>
|
||||
|
||||
{/* Floating Action Button */}
|
||||
<TouchableOpacity
|
||||
style={styles.fab}
|
||||
onPress={() => navigation.navigate('CreateCardBalanceOrder')}
|
||||
>
|
||||
<Ionicons name="add" size={28} color={COLORS.white} />
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: COLORS.backgroundSecondary,
|
||||
},
|
||||
centered: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
color: COLORS.textPrimary,
|
||||
},
|
||||
card: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: CARD_BG,
|
||||
marginHorizontal: 24,
|
||||
marginTop: 16,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
circle: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: CIRCLE_BG,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 16,
|
||||
},
|
||||
circleText: {
|
||||
color: COLORS.white,
|
||||
fontWeight: '600',
|
||||
},
|
||||
cardContent: {
|
||||
flex: 1,
|
||||
},
|
||||
cardNumber: {
|
||||
fontWeight: '700',
|
||||
color: COLORS.textPrimary,
|
||||
marginBottom: 4,
|
||||
},
|
||||
dateText: {
|
||||
color: COLORS.textSecondary,
|
||||
fontSize: 12,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 16,
|
||||
color: COLORS.textSecondary,
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
bottom: 32,
|
||||
right: 32,
|
||||
backgroundColor: COLORS.primary,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
elevation: 4,
|
||||
},
|
||||
passportText: {
|
||||
color: COLORS.textSecondary,
|
||||
fontSize: 14,
|
||||
marginBottom: 2,
|
||||
},
|
||||
});
|
||||
|
||||
export default CardBalanceOrdersScreen;
|
||||
134
src/screens/Card/CardOrderDetailsScreen.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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 { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import apiService from '../../services/apiService';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
import { useBaseEnums } from '../../contexts/BaseEnumsContext';
|
||||
|
||||
const DetailRow = ({ label, value, showBorder=true })=> (
|
||||
<View style={[styles.detailRow, showBorder && styles.detailRowBorder]}>
|
||||
<Text style={styles.detailKey}>{label}</Text>
|
||||
<Text style={styles.detailValue}>{String(value)}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const CardOrderDetailsScreen = ()=>{
|
||||
const navigation = useNavigation();
|
||||
const { params } = useRoute();
|
||||
const orderId = params?.orderId;
|
||||
const { getLabel, getBranches } = useBaseEnums();
|
||||
|
||||
const [loading,setLoading] = useState(true);
|
||||
const [order,setOrder] = useState(null);
|
||||
const [branchName,setBranchName]=useState('');
|
||||
|
||||
const fetchData = async()=>{
|
||||
setLoading(true);
|
||||
const res = await apiService.getCardOrder(orderId);
|
||||
if(res.success){
|
||||
setOrder(res.data);
|
||||
if(res.data?.region && res.data?.branch_id){
|
||||
try{ const brs = await getBranches(res.data.region); const found = brs.find(b=>String(b.id)===String(res.data.branch_id)); if(found) setBranchName(found.name);}catch(e){}
|
||||
}
|
||||
}else{ Alert.alert('Ýalňyşlyk', res.error || 'Maglumat almak bolmady'); }
|
||||
setLoading(false);
|
||||
};
|
||||
useEffect(()=>{ fetchData(); },[]);
|
||||
|
||||
const handleDelete = ()=>{ Alert.alert('Tassykla','Pozmakçy my?',[{text:'Goýbolsun',style:'cancel'},{text:'Poz',style:'destructive',onPress:async()=>{
|
||||
const r = await apiService.deleteCardOrder(orderId);
|
||||
if(r.success){ Alert.alert('Pozuldy',r.message||'Pozuldy',[{text:'OK',onPress:()=>navigation.goBack()}]); } else { Alert.alert('Ýalňyşlyk', r.error || 'Pozup bolmady'); }
|
||||
}}]); };
|
||||
|
||||
if(loading) return <View style={styles.center}><ActivityIndicator size="large" color={COLORS.primary}/></View>;
|
||||
if(!order) return <View style={styles.center}><Text>Maglumat ýok</Text></View>;
|
||||
|
||||
const formattedDate = (d)=> d? new Date(d).toLocaleDateString():'';
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar style="dark" />
|
||||
<TouchableOpacity style={styles.backBtn} onPress={()=>navigation.goBack()}><Ionicons name="close" size={28} color={COLORS.textPrimary}/></TouchableOpacity>
|
||||
|
||||
<ScrollView contentContainerStyle={{paddingBottom:120,paddingHorizontal:24}}>
|
||||
<Text style={styles.title}>Täze kart sargyt</Text>
|
||||
|
||||
{/* Summary */}
|
||||
<View style={styles.detailCard}>
|
||||
<DetailRow label="ID" value={order.unique_id} />
|
||||
<DetailRow label="Kartyň çykarylmagynyň sebäbi" value={getLabel('card_states', order.card_state_id)} />
|
||||
<DetailRow label="Kart görnüşi" value={getLabel('card_types', order.card_type_id)} />
|
||||
<DetailRow label="Tölenildi" value={order.paid? 'Hawa':'Ýok'} showBorder={false}/>
|
||||
</View>
|
||||
|
||||
{/* Location */}
|
||||
{(order.region || branchName) && (
|
||||
<>
|
||||
<Text style={styles.sectionTitle}>Lokasiýa</Text>
|
||||
<View style={styles.detailCard}>
|
||||
{order.region && <DetailRow label="Welaýat" value={getLabel('regions', order.region)} />}
|
||||
{(branchName||order.branch_id) && <DetailRow label="Şahamça" value={branchName || order.branch_id} showBorder={false}/>}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Personal */}
|
||||
<Text style={styles.sectionTitle}>Şahsy maglumatlar</Text>
|
||||
<View style={styles.detailCard}>
|
||||
<DetailRow label="Ady" value={order.customer_name} />
|
||||
<DetailRow label="Familiýasy" value={order.customer_surname} />
|
||||
{order.customer_patronic_name && <DetailRow label="Atasynyň ady" value={order.customer_patronic_name} />}
|
||||
{order.old_surname && <DetailRow label="Köne familiýasy" value={order.old_surname} />}
|
||||
<DetailRow label="Doglan güni" value={formattedDate(order.born_at)} />
|
||||
<DetailRow label="Telefon" value={`+993 ${order.phone}`} />
|
||||
{order.phone_additional && <DetailRow label="Telefon goşmaça" value={`+993 ${order.phone_additional}`}/>}
|
||||
|
||||
<DetailRow label="Ýazgy edilen salgyňyz" value={order.passport_address} />
|
||||
<DetailRow label="Häzirki ýaşaýyş ýeri" value={order.real_address} />
|
||||
<DetailRow label="Işleýän ýeriňiz we wezipäňiz" value={order.job_location} showBorder={false}/>
|
||||
</View>
|
||||
|
||||
{/* Passport info */}
|
||||
<Text style={styles.sectionTitle}>Pasport</Text>
|
||||
<View style={styles.detailCard}>
|
||||
<DetailRow label="Seriýa/Belgi" value={`${order.passport_serie} ${order.passport_id}`} />
|
||||
<DetailRow label="Berlen senesi" value={formattedDate(order.passport_given_at)} />
|
||||
<DetailRow label="Berlen ýeri" value={order.passport_given_by} />
|
||||
<DetailRow label="Doglan ýeri (passport)" value={order.born_place} style={{marginBottom:12}} showBorder={false}/>
|
||||
<View style={styles.imageGrid}>
|
||||
{order.passport_one && <TouchableOpacity onPress={()=>Linking.openURL(order.passport_one)} style={styles.imageWrapper}><Image source={{uri:order.passport_one}} style={styles.img}/><Text style={styles.imageLabel}>1-nji sah.</Text></TouchableOpacity>}
|
||||
{order.passport_two && <TouchableOpacity onPress={()=>Linking.openURL(order.passport_two)} style={styles.imageWrapper}><Image source={{uri:order.passport_two}} style={styles.img}/><Text style={styles.imageLabel}>2-3 sah.</Text></TouchableOpacity>}
|
||||
{order.passport_three && <TouchableOpacity onPress={()=>Linking.openURL(order.passport_three)} style={styles.imageWrapper}><Image source={{uri:order.passport_three}} style={styles.img}/><Text style={styles.imageLabel}>8-9 sah.</Text></TouchableOpacity>}
|
||||
{order.passport_four && <TouchableOpacity onPress={()=>Linking.openURL(order.passport_four)} style={styles.imageWrapper}><Image source={{uri:order.passport_four}} style={styles.img}/><Text style={styles.imageLabel}>32 sah.</Text></TouchableOpacity>}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.deleteBtn} onPress={handleDelete}><Text style={styles.deleteText}>Poz</Text></TouchableOpacity>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container:{flex:1,backgroundColor:COLORS.backgroundSecondary,paddingTop:40},
|
||||
center:{flex:1,justifyContent:'center',alignItems:'center'},
|
||||
backBtn:{alignSelf:'flex-end',marginRight:24,marginBottom:16},
|
||||
title:{fontSize:24,fontWeight:'bold',color:COLORS.textPrimary,marginBottom:24},
|
||||
detailCard:{backgroundColor:COLORS.white,borderRadius:12,padding:20,marginBottom:32},
|
||||
detailRow:{flexDirection:'row',justifyContent:'space-between',paddingVertical:12},
|
||||
detailRowBorder:{borderBottomWidth:1,borderBottomColor:COLORS.border},
|
||||
detailKey:{fontWeight:'600',color:COLORS.textSecondary},
|
||||
detailValue:{color:COLORS.textPrimary},
|
||||
sectionTitle:{fontWeight:'700',fontSize:18,marginTop:24,marginBottom:12,color:COLORS.textPrimary},
|
||||
imageGrid:{flexDirection:'row',flexWrap:'wrap',justifyContent:'space-between',marginTop:12},
|
||||
imageWrapper:{width:'48%',marginBottom:16},
|
||||
img:{width:'100%',aspectRatio:4/3,borderRadius:8,backgroundColor:'#eee'},
|
||||
imageLabel:{marginTop:4,fontSize:12,color:COLORS.textSecondary,textAlign:'center'},
|
||||
deleteBtn:{backgroundColor:COLORS.error,paddingVertical:16,borderRadius:8,alignItems:'center',marginTop:16},
|
||||
deleteText:{color:COLORS.white,fontSize:16,fontWeight:'600'},
|
||||
});
|
||||
|
||||
export default CardOrderDetailsScreen;
|
||||
85
src/screens/Card/CardOrdersScreen.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
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 { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import apiService from '../../services/apiService';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
import { useBaseEnums } from '../../contexts/BaseEnumsContext';
|
||||
|
||||
const CARD_BG = '#F1F9F1';
|
||||
const CIRCLE_BG = '#A2E4A4';
|
||||
|
||||
const CardOrdersScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const [orders,setOrders] = useState([]);
|
||||
const [loading,setLoading]=useState(true);
|
||||
const [refreshing,setRefreshing]=useState(false);
|
||||
const { getLabel } = useBaseEnums();
|
||||
|
||||
const fetchData = async()=>{
|
||||
try{
|
||||
const res = await apiService.getCardOrders();
|
||||
if(res.success) setOrders(Array.isArray(res.data)?res.data:[]);
|
||||
else console.warn(res.error);
|
||||
}catch(e){ console.warn(e.message);} finally {setLoading(false); setRefreshing(false);} };
|
||||
|
||||
useFocusEffect(useCallback(()=>{ fetchData(); },[]));
|
||||
|
||||
const onRefresh = ()=>{ setRefreshing(true); fetchData(); };
|
||||
|
||||
const handlePress = (item)=>{ navigation.navigate('CardOrderDetails',{orderId:item.id}); };
|
||||
|
||||
const renderItem=({item})=>{
|
||||
const created = item.created_at? new Date(item.created_at).toLocaleDateString():'';
|
||||
return (
|
||||
<TouchableOpacity style={styles.card} onPress={()=>handlePress(item)}>
|
||||
<View style={styles.circle}><Text style={styles.circleText}>{item.id}</Text></View>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.titleText}>{item.unique_id}</Text>
|
||||
<Text style={styles.line}>Kart görnüşi: {getLabel('card_types',item.card_type_id)}</Text>
|
||||
<Text style={styles.line}>Tölenildi: {item.paid? 'Hawa':'Ýok'}</Text>
|
||||
<Text style={styles.status}>{item.status}</Text>
|
||||
<Text style={styles.date}>{created}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
); };
|
||||
|
||||
if(loading) return <View style={styles.center}><ActivityIndicator size="large" color={COLORS.primary}/></View>;
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar style="dark" />
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={()=>navigation.goBack()} style={{paddingRight:12}}>
|
||||
<Ionicons name="arrow-back" size={24} color={COLORS.textPrimary}/>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>Täze kart sargytlary</Text>
|
||||
</View>
|
||||
<FlatList data={orders} keyExtractor={i=>i.id.toString()} renderItem={renderItem} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh}/>} contentContainerStyle={orders.length===0 && styles.empty} ListEmptyComponent={<Text style={styles.emptyText}>Sargyt ýok</Text>} />
|
||||
<TouchableOpacity style={styles.fab} onPress={()=>navigation.navigate('CreateCardOrder')}>
|
||||
<Ionicons name="add" size={28} color={COLORS.white}/>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
); };
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container:{flex:1,backgroundColor:COLORS.backgroundSecondary},
|
||||
header:{flexDirection:'row',alignItems:'center',paddingHorizontal:24,paddingVertical:16},
|
||||
headerTitle:{fontSize:20,fontWeight:'bold',color:COLORS.textPrimary},
|
||||
center:{flex:1,justifyContent:'center',alignItems:'center'},
|
||||
card:{flexDirection:'row',backgroundColor:CARD_BG,marginHorizontal:24,marginTop:16,borderRadius:12,padding:16},
|
||||
circle:{width:40,height:40,borderRadius:20,backgroundColor:CIRCLE_BG,alignItems:'center',justifyContent:'center',marginRight:16},
|
||||
circleText:{color:COLORS.white,fontWeight:'600'},
|
||||
content:{flex:1},
|
||||
titleText:{fontWeight:'700',color:COLORS.textPrimary},
|
||||
line:{fontSize:12,color:COLORS.textSecondary},
|
||||
status:{fontSize:13,fontWeight:'600',marginTop:2},
|
||||
date:{fontSize:11,color:COLORS.textSecondary},
|
||||
fab:{position:'absolute',bottom:32,right:32,width:56,height:56,borderRadius:28,backgroundColor:COLORS.primary,alignItems:'center',justifyContent:'center',elevation:4},
|
||||
empty:{flex:1,justifyContent:'center',alignItems:'center'},
|
||||
emptyText:{color:COLORS.textSecondary,fontSize:16},
|
||||
});
|
||||
|
||||
export default CardOrdersScreen;
|
||||
146
src/screens/Card/CardPinOrderDetailsScreen.js
Normal file
@@ -0,0 +1,146 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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 { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import apiService from '../../services/apiService';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
import { useBaseEnums } from '../../contexts/BaseEnumsContext';
|
||||
|
||||
const DetailRow = ({ label, value, showBorder=true }) => (
|
||||
<View style={[styles.detailRow, showBorder && styles.detailRowBorder]}>
|
||||
<Text style={styles.detailKey}>{label}</Text>
|
||||
<Text style={styles.detailValue}>{String(value)}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const CardPinOrderDetailsScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const { params } = useRoute();
|
||||
const orderId = params?.orderId;
|
||||
const { getLabel, getBranches } = useBaseEnums();
|
||||
|
||||
const [loading,setLoading]=useState(true);
|
||||
const [order,setOrder]=useState(null);
|
||||
const [branchName,setBranchName]=useState('');
|
||||
|
||||
const fetchDetails=async()=>{
|
||||
setLoading(true);
|
||||
const res = await apiService.getCardPinOrder(orderId);
|
||||
if(res.success){
|
||||
setOrder(res.data);
|
||||
if(res.data?.region && res.data?.branch_id){
|
||||
(async()=>{ try{ const brs = await getBranches(res.data.region); const found=brs.find(b=>String(b.id)===String(res.data.branch_id)); if(found) setBranchName(found.name);}catch(e){} })();
|
||||
}
|
||||
} else { Alert.alert('Ýalňyşlyk', res.error || 'Ýalňyşlyk'); }
|
||||
setLoading(false);
|
||||
};
|
||||
useEffect(()=>{fetchDetails();},[]);
|
||||
|
||||
const handleDelete=()=>{ Alert.alert('Tassykla','Pozmakçy my?',[{text:'Goýbolsun',style:'cancel'},{text:'Poz',style:'destructive',onPress:async()=>{
|
||||
const r=await apiService.deleteCardPinOrder(orderId);
|
||||
if(r.success){ Alert.alert('Pozuldy', r.message || 'Pozuldy', [{text:'OK',onPress:()=>navigation.goBack()}]); } else { Alert.alert('Ýalňyşlyk', r.error || 'Ýalňyşlyk'); }
|
||||
}}]); };
|
||||
|
||||
if(loading){ return <View style={styles.centered}><ActivityIndicator size="large" color={COLORS.primary}/></View>; }
|
||||
if(!order){ return <View style={styles.centered}><Text>Maglumat ýok</Text></View>; }
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar style="dark" />
|
||||
<TouchableOpacity style={styles.backBtn} onPress={()=>navigation.goBack()}>
|
||||
<Ionicons name="close" size={28} color={COLORS.textPrimary}/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<ScrollView contentContainerStyle={{paddingBottom:120,paddingHorizontal:24}}>
|
||||
<Text style={styles.title}>Kart pin bukjalar</Text>
|
||||
|
||||
{/* Card info */}
|
||||
<View style={styles.detailCard}>
|
||||
<DetailRow label="ID" value={order.id} />
|
||||
{order.card_type_id && <DetailRow label="Görnüşi" value={getLabel('card_types', order.card_type_id)} />}
|
||||
{order.card_number && <DetailRow label="Kart belgisi" value={order.card_number} showBorder={false} />}
|
||||
</View>
|
||||
|
||||
{/* Location */}
|
||||
{(order.region || branchName) && (
|
||||
<>
|
||||
<Text style={styles.sectionTitle}>Lokasiýa</Text>
|
||||
<View style={styles.detailCard}>
|
||||
{order.region && <DetailRow label="Welaýat" value={getLabel('regions', order.region)} />}
|
||||
{(branchName || order.branch_id) && <DetailRow label="Şahamça" value={branchName || order.branch_id} showBorder={false} />}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Personal */}
|
||||
<Text style={styles.sectionTitle}>Şahsy maglumatlar</Text>
|
||||
<View style={styles.detailCard}>
|
||||
{order.customer_name && <DetailRow label="Ady" value={order.customer_name} />}
|
||||
{order.customer_surname && <DetailRow label="Familiýasy" value={order.customer_surname} />}
|
||||
{order.customer_patronic_name && <DetailRow label="Atasynyň ady" value={order.customer_patronic_name} />}
|
||||
{order.born_at && <DetailRow label="Doglan güni" value={order.born_at} />}
|
||||
{order.phone && <DetailRow label="Telefon" value={`+993 ${order.phone}`} showBorder={false} />}
|
||||
</View>
|
||||
|
||||
{/* Passport */}
|
||||
<Text style={styles.sectionTitle}>Pasport</Text>
|
||||
<View style={styles.detailCard}>
|
||||
{(order.passport_serie || order.passport_id) && <DetailRow label="Seriýa/Belgi" value={`${order.passport_serie || ''} ${order.passport_id || ''}`.trim()} showBorder={false} />}
|
||||
<View style={styles.imageGrid}>
|
||||
{order.passport_one && (
|
||||
<TouchableOpacity onPress={()=>Linking.openURL(order.passport_one)} style={styles.imageWrapper}>
|
||||
<Image source={{uri:order.passport_one}} style={styles.passportImage}/>
|
||||
<Text style={styles.imageLabel}>1-nji sah.</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{order.passport_two && (
|
||||
<TouchableOpacity onPress={()=>Linking.openURL(order.passport_two)} style={styles.imageWrapper}>
|
||||
<Image source={{uri:order.passport_two}} style={styles.passportImage}/>
|
||||
<Text style={styles.imageLabel}>2-3 sah.</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{order.passport_three && (
|
||||
<TouchableOpacity onPress={()=>Linking.openURL(order.passport_three)} style={styles.imageWrapper}>
|
||||
<Image source={{uri:order.passport_three}} style={styles.passportImage}/>
|
||||
<Text style={styles.imageLabel}>8-9 sah.</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{order.passport_four && (
|
||||
<TouchableOpacity onPress={()=>Linking.openURL(order.passport_four)} style={styles.imageWrapper}>
|
||||
<Image source={{uri:order.passport_four}} style={styles.passportImage}/>
|
||||
<Text style={styles.imageLabel}>32 sah.</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.deleteBtn} onPress={handleDelete}>
|
||||
<Text style={styles.deleteText}>Poz</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container:{flex:1,backgroundColor:COLORS.backgroundSecondary,paddingTop:40},
|
||||
centered:{flex:1,justifyContent:'center',alignItems:'center'},
|
||||
backBtn:{alignSelf:'flex-end',marginRight:24,marginBottom:16},
|
||||
title:{fontSize:24,fontWeight:'bold',color:COLORS.textPrimary,marginBottom:24},
|
||||
detailCard:{backgroundColor:COLORS.white,borderRadius:12,padding:20,marginBottom:32},
|
||||
detailRow:{flexDirection:'row',justifyContent:'space-between',paddingVertical:12},
|
||||
detailRowBorder:{borderBottomWidth:1,borderBottomColor:COLORS.border},
|
||||
detailKey:{fontWeight:'600',color:COLORS.textSecondary},
|
||||
detailValue:{color:COLORS.textPrimary},
|
||||
sectionTitle:{fontWeight:'700',fontSize:18,marginTop:24,marginBottom:12,color:COLORS.textPrimary},
|
||||
imageGrid:{flexDirection:'row',flexWrap:'wrap',justifyContent:'space-between',marginTop:12},
|
||||
imageWrapper:{width:'48%',marginBottom:16},
|
||||
passportImage:{width:'100%',aspectRatio:4/3,borderRadius:8,backgroundColor:'#f0f0f0'},
|
||||
imageLabel:{marginTop:4,fontSize:12,color:COLORS.textSecondary,textAlign:'center'},
|
||||
deleteBtn:{backgroundColor:COLORS.error,paddingVertical:16,borderRadius:8,alignItems:'center',marginTop:16},
|
||||
deleteText:{color:COLORS.white,fontSize:16,fontWeight:'600'},
|
||||
});
|
||||
|
||||
export default CardPinOrderDetailsScreen;
|
||||
124
src/screens/Card/CardPinOrdersScreen.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
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 { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import apiService from '../../services/apiService';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
import { useBaseEnums } from '../../contexts/BaseEnumsContext';
|
||||
|
||||
const CARD_BG = '#F1F9F1';
|
||||
const CIRCLE_BG = '#A2E4A4';
|
||||
|
||||
const CardPinOrdersScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const [orders, setOrders] = useState([]);
|
||||
const { getLabel } = useBaseEnums();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const fetchOrders = async () => {
|
||||
try {
|
||||
const res = await apiService.getCardPinOrders();
|
||||
if (res.success) {
|
||||
setOrders(Array.isArray(res.data) ? res.data : []);
|
||||
} else {
|
||||
console.warn(res.error);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fetchOrders();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const onRefresh = () => {
|
||||
setRefreshing(true);
|
||||
fetchOrders();
|
||||
};
|
||||
|
||||
const handleItemPress = (item) => {
|
||||
navigation.navigate('CardPinOrderDetails', { orderId: item.id });
|
||||
};
|
||||
|
||||
const renderItem = ({ item }) => {
|
||||
const masked = item.card_number ? `${item.card_number.slice(0,6)}******${item.card_number.slice(-4)}` : '';
|
||||
const created = item.created_at ? new Date(item.created_at).toLocaleDateString() : '';
|
||||
const typeLabel = getLabel('card_types', item.card_type_id);
|
||||
const passportLine = item.passport_serie && item.passport_id ? `${item.passport_serie} ${item.passport_id}` : '';
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={styles.card} onPress={() => handleItemPress(item)}>
|
||||
<View style={styles.circle}>
|
||||
<Text style={styles.circleText}>{item.id}</Text>
|
||||
</View>
|
||||
<View style={styles.cardContent}>
|
||||
<Text style={styles.cardNumber}>{masked}</Text>
|
||||
{typeLabel && <Text style={styles.typeText}>{typeLabel}</Text>}
|
||||
{passportLine !== '' && <Text style={styles.passportText}>{passportLine}</Text>}
|
||||
<Text style={styles.dateText}>{created}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator size="large" color={COLORS.primary} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar style="dark" />
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => navigation.goBack()} style={{ paddingRight: 12 }}>
|
||||
<Ionicons name="arrow-back" size={24} color={COLORS.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>Kart pin bukjalar</Text>
|
||||
</View>
|
||||
<FlatList
|
||||
data={orders}
|
||||
keyExtractor={(item) => item.id?.toString()}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={orders.length === 0 && styles.emptyContainer}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
||||
ListEmptyComponent={<Text style={styles.emptyText}>Heniz sargyt ýok</Text>}
|
||||
/>
|
||||
|
||||
<TouchableOpacity style={styles.fab} onPress={() => navigation.navigate('CreateCardPinOrder')}>
|
||||
<Ionicons name="add" size={28} color={COLORS.white} />
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: COLORS.backgroundSecondary },
|
||||
centered: { flex: 1, alignItems: 'center', justifyContent: 'center' },
|
||||
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 24, paddingVertical: 16 },
|
||||
headerTitle: { fontSize: 20, fontWeight: 'bold', color: COLORS.textPrimary },
|
||||
card: { flexDirection: 'row', backgroundColor: CARD_BG, marginHorizontal: 24, marginTop: 16, borderRadius: 12, padding: 16, alignItems: 'center' },
|
||||
circle: { width: 40, height: 40, borderRadius: 20, backgroundColor: CIRCLE_BG, alignItems: 'center', justifyContent: 'center', marginRight: 16 },
|
||||
circleText: { color: COLORS.white, fontWeight: '600' },
|
||||
cardContent: { flex: 1 },
|
||||
cardNumber: { fontWeight: '700', color: COLORS.textPrimary, marginBottom: 4 },
|
||||
dateText: { color: COLORS.textSecondary, fontSize: 12 },
|
||||
emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
emptyText: { fontSize: 16, color: COLORS.textSecondary },
|
||||
fab: { position: 'absolute', bottom: 32, right: 32, backgroundColor: COLORS.primary, width: 56, height: 56, borderRadius: 28, alignItems: 'center', justifyContent: 'center', elevation: 4 },
|
||||
typeText:{color:COLORS.textSecondary,fontSize:14},
|
||||
passportText:{color:COLORS.textSecondary,fontSize:12},
|
||||
});
|
||||
|
||||
export default CardPinOrdersScreen;
|
||||
173
src/screens/Card/CardRequisiteOrderDetailsScreen.js
Normal file
@@ -0,0 +1,173 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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 { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import apiService from '../../services/apiService';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
import { Linking } from 'react-native';
|
||||
import { useBaseEnums } from '../../contexts/BaseEnumsContext';
|
||||
|
||||
const DetailRow = ({ label, value, showBorder=true }) => (
|
||||
<View style={[styles.detailRow, showBorder && styles.detailRowBorder]}>
|
||||
<Text style={styles.detailKey}>{label}</Text>
|
||||
<Text style={styles.detailValue}>{String(value)}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const CardRequisiteOrderDetailsScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const { params } = useRoute();
|
||||
const orderId = params?.orderId;
|
||||
|
||||
const { getLabel, getBranches } = useBaseEnums();
|
||||
|
||||
const [loading,setLoading]=useState(true);
|
||||
const [order,setOrder]=useState(null);
|
||||
const [downloading,setDownloading]=useState(false);
|
||||
const [branchName,setBranchName]=useState('');
|
||||
|
||||
const fetchDetails=async()=>{
|
||||
setLoading(true);
|
||||
const res = await apiService.getCardRequisiteOrder(orderId);
|
||||
if(res.success){
|
||||
setOrder(res.data);
|
||||
// try to resolve branch name if region & branch_id exist
|
||||
if(res.data?.region && res.data?.branch_id){
|
||||
(async()=>{
|
||||
try{
|
||||
const brs = await getBranches(res.data.region);
|
||||
const found = brs.find(b=>String(b.id)===String(res.data.branch_id));
|
||||
if(found) setBranchName(found.name);
|
||||
}catch(e){ /* silent */ }
|
||||
})();
|
||||
}
|
||||
} else { Alert.alert('Error',res.error||'Failed'); }
|
||||
setLoading(false);
|
||||
};
|
||||
useEffect(()=>{fetchDetails();},[]);
|
||||
|
||||
const handleDownload=async()=>{
|
||||
setDownloading(true);
|
||||
const res = await apiService.downloadCardRequisites(orderId);
|
||||
setDownloading(false);
|
||||
if(res.success && res.data?.url){ Linking.openURL(res.data.url); } else { Alert.alert('Ýalňyşlyk', res.error || 'Ýüklenip bilinmedi'); }
|
||||
};
|
||||
|
||||
const handleDelete=()=>{ Alert.alert('Tassykla','Pozmakçy my?',[{text:'Goýbolsun',style:'cancel'},{text:'Poz',style:'destructive',onPress:async()=>{
|
||||
const r=await apiService.deleteCardRequisiteOrder(orderId);
|
||||
if(r.success){ Alert.alert('Pozuldy',r.message||'Pozuldy',[{text:'OK',onPress:()=>navigation.goBack()}]); } else {Alert.alert('Ýalňyşlyk',r.error||'Ýalňyşlyk');}
|
||||
}}]); };
|
||||
|
||||
if(loading){return <View style={styles.centered}><ActivityIndicator size="large" color={COLORS.primary}/></View>;}
|
||||
if(!order){return <View style={styles.centered}><Text>Maglumat ýok</Text></View>;}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar style="dark" />
|
||||
<TouchableOpacity style={styles.backBtn} onPress={()=>navigation.goBack()}>
|
||||
<Ionicons name="close" size={28} color={COLORS.textPrimary}/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<ScrollView contentContainerStyle={{paddingBottom:120,paddingHorizontal:24}}>
|
||||
<Text style={styles.title}>Kart rekwizitler</Text>
|
||||
|
||||
{/* Card info */}
|
||||
<View style={styles.detailCard}>
|
||||
<DetailRow label="ID" value={order.id}/>
|
||||
{order.card_type_id && <DetailRow label="Görnüşi" value={getLabel('card_types', order.card_type_id)} />}
|
||||
{order.card_name && <DetailRow label="Kartdaky ady" value={order.card_name} />}
|
||||
{order.card_number && <DetailRow label="Kart belgisi" value={order.card_number} />}
|
||||
{order.card_month && order.card_year && <DetailRow label="Möhleti" value={`${order.card_month}/${order.card_year}`} showBorder={false} />}
|
||||
</View>
|
||||
|
||||
{/* Location */}
|
||||
{(order.region || branchName) && (
|
||||
<>
|
||||
<Text style={styles.sectionTitle}>Lokasiýa</Text>
|
||||
<View style={styles.detailCard}>
|
||||
{order.region && <DetailRow label="Welaýat" value={getLabel('regions', order.region)} />}
|
||||
{(branchName || order.branch_id) && <DetailRow label="Şahamça" value={branchName || order.branch_id} showBorder={false} />}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Personal info */}
|
||||
<Text style={styles.sectionTitle}>Şahsy maglumatlar</Text>
|
||||
<View style={styles.detailCard}>
|
||||
{order.customer_name && <DetailRow label="Ady" value={order.customer_name} />}
|
||||
{order.customer_surname && <DetailRow label="Familiýasy" value={order.customer_surname} />}
|
||||
{order.customer_patronic_name && <DetailRow label="Atasynyň ady" value={order.customer_patronic_name} />}
|
||||
{order.born_at && <DetailRow label="Doglan güni" value={order.born_at} />}
|
||||
{order.phone && <DetailRow label="Telefon" value={`+993 ${order.phone}`} showBorder={false} />}
|
||||
</View>
|
||||
|
||||
{/* Passport */}
|
||||
<Text style={styles.sectionTitle}>Pasport</Text>
|
||||
<View style={styles.detailCard}>
|
||||
{(order.passport_serie || order.passport_id) && <DetailRow label="Seriýa/Belgi" value={`${order.passport_serie || ''} ${order.passport_id || ''}`.trim()} showBorder={false}/>}
|
||||
|
||||
{/* Image thumbnails */}
|
||||
<View style={styles.imageGrid}>
|
||||
{order.passport_one && (
|
||||
<TouchableOpacity onPress={()=>Linking.openURL(order.passport_one)} style={styles.imageWrapper}>
|
||||
<Image source={{ uri: order.passport_one }} style={styles.passportImage} resizeMode="cover" />
|
||||
<Text style={styles.imageLabel}>1-nji sah.</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{order.passport_two && (
|
||||
<TouchableOpacity onPress={()=>Linking.openURL(order.passport_two)} style={styles.imageWrapper}>
|
||||
<Image source={{ uri: order.passport_two }} style={styles.passportImage} resizeMode="cover" />
|
||||
<Text style={styles.imageLabel}>2-3 sah.</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{order.passport_three && (
|
||||
<TouchableOpacity onPress={()=>Linking.openURL(order.passport_three)} style={styles.imageWrapper}>
|
||||
<Image source={{ uri: order.passport_three }} style={styles.passportImage} resizeMode="cover" />
|
||||
<Text style={styles.imageLabel}>8-9 sah.</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{order.passport_four && (
|
||||
<TouchableOpacity onPress={()=>Linking.openURL(order.passport_four)} style={styles.imageWrapper}>
|
||||
<Image source={{ uri: order.passport_four }} style={styles.passportImage} resizeMode="cover" />
|
||||
<Text style={styles.imageLabel}>32 sah.</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Actions */}
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={handleDownload}>
|
||||
<Text style={styles.actionText}>Ýükle (PDF)</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.deleteBtn} onPress={handleDelete}>
|
||||
<Text style={styles.deleteText}>Poz</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container:{flex:1,backgroundColor:COLORS.backgroundSecondary,paddingTop:40},
|
||||
centered:{flex:1,justifyContent:'center',alignItems:'center'},
|
||||
backBtn:{alignSelf:'flex-end',marginRight:24,marginBottom:16},
|
||||
title:{fontSize:24,fontWeight:'bold',color:COLORS.textPrimary,marginBottom:24},
|
||||
detailCard:{backgroundColor:COLORS.white,borderRadius:12,padding:20,marginBottom:32},
|
||||
detailRow:{flexDirection:'row',justifyContent:'space-between',paddingVertical:12},
|
||||
detailRowBorder:{borderBottomWidth:1,borderBottomColor:COLORS.border},
|
||||
detailKey:{fontWeight:'600',color:COLORS.textSecondary},
|
||||
detailValue:{color:COLORS.textPrimary},
|
||||
sectionTitle:{fontWeight:'700',fontSize:18,marginTop:24,marginBottom:12,color:COLORS.textPrimary},
|
||||
actionBtn:{backgroundColor:COLORS.primary,paddingVertical:16,borderRadius:8,alignItems:'center',marginBottom:16},
|
||||
actionText:{color:COLORS.white,fontSize:16,fontWeight:'600'},
|
||||
deleteBtn:{backgroundColor:COLORS.error,paddingVertical:16,borderRadius:8,alignItems:'center'},
|
||||
deleteText:{color:COLORS.white,fontSize:16,fontWeight:'600'},
|
||||
imageGrid:{flexDirection:'row',flexWrap:'wrap',justifyContent:'space-between',marginTop:12},
|
||||
imageWrapper:{width:'48%',marginBottom:16},
|
||||
passportImage:{width:'100%',aspectRatio:4/3,borderRadius:8,backgroundColor:COLORS.gray?COLORS.gray[200]:"#f0f0f0"},
|
||||
imageLabel:{marginTop:4,fontSize:12,color:COLORS.textSecondary,textAlign:'center'},
|
||||
});
|
||||
|
||||
export default CardRequisiteOrderDetailsScreen;
|
||||
124
src/screens/Card/CardRequisiteOrdersScreen.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
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 { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import apiService from '../../services/apiService';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
import { useBaseEnums } from '../../contexts/BaseEnumsContext';
|
||||
|
||||
const CARD_BG = '#F1F9F1';
|
||||
const CIRCLE_BG = '#A2E4A4';
|
||||
|
||||
const CardRequisiteOrdersScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const [orders, setOrders] = useState([]);
|
||||
const { getLabel } = useBaseEnums();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const fetchOrders = async () => {
|
||||
try {
|
||||
const res = await apiService.getCardRequisiteOrders();
|
||||
if (res.success) {
|
||||
setOrders(Array.isArray(res.data) ? res.data : []);
|
||||
} else {
|
||||
console.warn(res.error);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fetchOrders();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const onRefresh = () => {
|
||||
setRefreshing(true);
|
||||
fetchOrders();
|
||||
};
|
||||
|
||||
const handleItemPress = (item) => {
|
||||
navigation.navigate('CardRequisiteOrderDetails', { orderId: item.id });
|
||||
};
|
||||
|
||||
const renderItem = ({ item }) => {
|
||||
const masked = item.card_mask_number || (item.card_number ? `${item.card_number.slice(0,6)}******${item.card_number.slice(-4)}` : '');
|
||||
const created = item.created_at ? new Date(item.created_at).toLocaleDateString() : '';
|
||||
const typeLabel = getLabel('card_types', item.card_type_id);
|
||||
const passportLine = item.passport_serie && item.passport_id ? `${item.passport_serie} ${item.passport_id}` : '';
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={styles.card} onPress={() => handleItemPress(item)}>
|
||||
<View style={styles.circle}>
|
||||
<Text style={styles.circleText}>{item.id}</Text>
|
||||
</View>
|
||||
<View style={styles.cardContent}>
|
||||
<Text style={styles.cardNumber}>{masked}</Text>
|
||||
{typeLabel && <Text style={styles.typeText}>{typeLabel}</Text>}
|
||||
{passportLine !== '' && <Text style={styles.passportText}>{passportLine}</Text>}
|
||||
<Text style={styles.dateText}>{created}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator size="large" color={COLORS.primary} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar style="dark" />
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => navigation.goBack()} style={{ paddingRight: 12 }}>
|
||||
<Ionicons name="arrow-back" size={24} color={COLORS.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>Kart rekwizitler</Text>
|
||||
</View>
|
||||
<FlatList
|
||||
data={orders}
|
||||
keyExtractor={(item) => item.id?.toString()}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={orders.length === 0 && styles.emptyContainer}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
||||
ListEmptyComponent={<Text style={styles.emptyText}>Maglumat ýok</Text>}
|
||||
/>
|
||||
|
||||
<TouchableOpacity style={styles.fab} onPress={() => navigation.navigate('CreateCardRequisiteOrder')}>
|
||||
<Ionicons name="add" size={28} color={COLORS.white} />
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: COLORS.backgroundSecondary },
|
||||
centered: { flex: 1, alignItems: 'center', justifyContent: 'center' },
|
||||
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 24, paddingVertical: 16 },
|
||||
headerTitle: { fontSize: 20, fontWeight: 'bold', color: COLORS.textPrimary },
|
||||
card: { flexDirection: 'row', backgroundColor: CARD_BG, marginHorizontal: 24, marginTop: 16, borderRadius: 12, padding: 16, alignItems: 'center' },
|
||||
circle: { width: 40, height: 40, borderRadius: 20, backgroundColor: CIRCLE_BG, alignItems: 'center', justifyContent: 'center', marginRight: 16 },
|
||||
circleText: { color: COLORS.white, fontWeight: '600' },
|
||||
cardContent: { flex: 1 },
|
||||
cardNumber: { fontWeight: '700', color: COLORS.textPrimary, marginBottom: 4 },
|
||||
dateText: { color: COLORS.textSecondary, fontSize: 12 },
|
||||
emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
emptyText: { fontSize: 16, color: COLORS.textSecondary },
|
||||
fab: { position: 'absolute', bottom: 32, right: 32, backgroundColor: COLORS.primary, width: 56, height: 56, borderRadius: 28, alignItems: 'center', justifyContent: 'center', elevation: 4 },
|
||||
typeText:{color:COLORS.textSecondary,fontSize:14},
|
||||
passportText:{color:COLORS.textSecondary,fontSize:12},
|
||||
});
|
||||
|
||||
export default CardRequisiteOrdersScreen;
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import apiService from '../../services/apiService';
|
||||
@@ -78,7 +79,7 @@ const CardTransactionOrderDetailsScreen = () => {
|
||||
Linking.openURL(res.data.url);
|
||||
setModalVisible(false);
|
||||
} else {
|
||||
Alert.alert('Error', res.error || 'Could not download');
|
||||
Alert.alert('Ýalňyşlyk', res.data.message || 'Näsazlyk ýüze çykdy');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,7 +94,7 @@ const CardTransactionOrderDetailsScreen = () => {
|
||||
if (!order) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<Text>No data</Text>
|
||||
<Text>Maglumat tapylmady</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import apiService from '../../services/apiService';
|
||||
|
||||
172
src/screens/Card/CreateCardBalanceOrderScreen.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 { useNavigation } from '@react-navigation/native';
|
||||
import apiService from '../../services/apiService';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
import Input from '../../components/Input';
|
||||
import SelectInput from '../../components/SelectInput';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const monthOptions = Array.from({ length: 12 }).map((_, i) => {
|
||||
const m = String(i + 1).padStart(2, '0');
|
||||
return { label: m, value: m };
|
||||
});
|
||||
const yearOptions = Array.from({ length: 60 }).map((_, i) => {
|
||||
const y = String(new Date().getFullYear() + i);
|
||||
return { label: y, value: y };
|
||||
});
|
||||
|
||||
const PASSPORT_SERIES = ['I-AS','I-MR','II-MR','I-AH','II-AH','I-LB','II-LB','I-BN','II-BN','I-DZ','II-DZ'];
|
||||
|
||||
// Helper to format card number with spaces
|
||||
const formatCardNumber = (value) => {
|
||||
const digits = String(value).replace(/[^0-9]/g, '').slice(0, 16);
|
||||
return (digits.match(/.{1,4}/g) || []).join(' ');
|
||||
};
|
||||
|
||||
const CreateCardBalanceOrderScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const [cardNumber, setCardNumber] = useState('');
|
||||
const [cardMonth, setCardMonth] = useState('');
|
||||
const [cardYear, setCardYear] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { user } = useAuth();
|
||||
const [passportSerie, setPassportSerie] = useState('');
|
||||
const [passportId, setPassportId] = useState('');
|
||||
|
||||
const handleCardNumberChange = (value) => {
|
||||
// Keep only digits
|
||||
const digits = value.replace(/[^0-9]/g, '').slice(0, 16);
|
||||
// Group into chunks of 4
|
||||
const parts = digits.match(/.{1,4}/g) || [];
|
||||
const formatted = parts.join(' ');
|
||||
setCardNumber(formatted);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
if (user.passport_serie) setPassportSerie(user.passport_serie);
|
||||
if (user.passport_id) setPassportId(String(user.passport_id));e
|
||||
|
||||
if (user.card_number) setCardNumber(formatCardNumber(user.card_number));
|
||||
if (user.card_month) setCardMonth(String(user.card_month).padStart(2, '0'));
|
||||
if (user.card_year) setCardYear(String(user.card_year));
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!cardNumber.trim() || !cardMonth || !cardYear || !passportSerie || !passportId.trim()) {
|
||||
Alert.alert('Error', 'All fields are required');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
// Remove spaces before sending
|
||||
const rawCard = cardNumber.replace(/\s+/g, '');
|
||||
const res = await apiService.createCardBalanceOrder(rawCard, cardMonth, cardYear, passportSerie, passportId.trim());
|
||||
setLoading(false);
|
||||
if (res.success) {
|
||||
Alert.alert('Success', res.message || 'Order created successfully', [
|
||||
{ text: 'OK', onPress: () => navigation.goBack() },
|
||||
]);
|
||||
} else {
|
||||
Alert.alert('Error', res.error || 'Could not create order');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar style="dark" />
|
||||
<ScrollView contentContainerStyle={{ paddingHorizontal: 24, paddingTop: 40, paddingBottom: 40 }}>
|
||||
<TouchableOpacity style={styles.backBtn} onPress={() => navigation.goBack()}>
|
||||
<Ionicons name="arrow-back" size={24} color={COLORS.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={styles.title}>Täze sargyt</Text>
|
||||
|
||||
<SelectInput
|
||||
label="*Passport seriýasy"
|
||||
value={passportSerie}
|
||||
onValueChange={setPassportSerie}
|
||||
options={PASSPORT_SERIES.map((v) => ({ label: v, value: v }))}
|
||||
placeholder="Saýla"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="*Passport belgisi"
|
||||
placeholder="123456"
|
||||
value={passportId}
|
||||
onChangeText={setPassportId}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="*Kart belgisi"
|
||||
placeholder="9934 6121 0000 0243"
|
||||
value={cardNumber}
|
||||
onChangeText={handleCardNumberChange}
|
||||
keyboardType="numeric"
|
||||
maxLength={19} // 16 digits + 3 spaces
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label="*Aý"
|
||||
value={cardMonth}
|
||||
onValueChange={setCardMonth}
|
||||
options={monthOptions}
|
||||
placeholder="Saýla"
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label="*Ýyl"
|
||||
value={cardYear}
|
||||
onValueChange={setCardYear}
|
||||
options={yearOptions}
|
||||
placeholder="Saýla"
|
||||
/>
|
||||
|
||||
<TouchableOpacity style={styles.submitBtn} onPress={handleSubmit} disabled={loading}>
|
||||
{loading ? (
|
||||
<ActivityIndicator color={COLORS.white} />
|
||||
) : (
|
||||
<Text style={styles.submitText}>Ýatda sakla</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: COLORS.backgroundSecondary,
|
||||
},
|
||||
backBtn: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: COLORS.textPrimary,
|
||||
marginBottom: 24,
|
||||
},
|
||||
submitBtn: {
|
||||
marginTop: 32,
|
||||
backgroundColor: COLORS.primary,
|
||||
paddingVertical: 16,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
submitText: {
|
||||
color: COLORS.white,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export default CreateCardBalanceOrderScreen;
|
||||
202
src/screens/Card/CreateCardOrderScreen.js
Normal file
@@ -0,0 +1,202 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
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 { useNavigation } from '@react-navigation/native';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import apiService from '../../services/apiService';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
import Input from '../../components/Input';
|
||||
import SelectInput from '../../components/SelectInput';
|
||||
import DateInput from '../../components/DateInput';
|
||||
import ImageInput from '../../components/ImageInput';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useBaseEnums } from '../../contexts/BaseEnumsContext';
|
||||
import { WebView } from 'react-native-webview';
|
||||
|
||||
const PASSPORT_SERIES = ['I-AS','I-MR','II-MR','I-AH','II-AH','I-LB','II-LB','I-BN','II-BN','I-DZ','II-DZ'];
|
||||
|
||||
const CreateCardOrderScreen = ()=>{
|
||||
const navigation=useNavigation();
|
||||
const { user } = useAuth();
|
||||
const { getOptions, getBranches } = useBaseEnums();
|
||||
|
||||
const [cardState,setCardState]=useState('');
|
||||
const [cardType,setCardType]=useState('');
|
||||
const [region,setRegion]=useState('');
|
||||
const [branchId,setBranchId]=useState('');
|
||||
|
||||
const [customerName,setCustomerName]=useState('');
|
||||
const [customerSurname,setCustomerSurname]=useState('');
|
||||
const [customerPatro,setCustomerPatro]=useState('');
|
||||
const [oldSurname,setOldSurname]=useState('');
|
||||
const [bornAt,setBornAt]=useState('');
|
||||
const [phone,setPhone]=useState('');
|
||||
const [phoneAdditional,setPhoneAdditional]=useState('');
|
||||
const [citizenship] = useState('');
|
||||
const [passportSerie,setPassportSerie]=useState('');
|
||||
const [passportId,setPassportId]=useState('');
|
||||
const [passportGivenAt,setPassportGivenAt]=useState('');
|
||||
const [passportGivenBy,setPassportGivenBy]=useState('');
|
||||
const [bornPlace,setBornPlace]=useState('');
|
||||
const [passportAddress,setPassportAddress]=useState('');
|
||||
const [realAddress,setRealAddress]=useState('');
|
||||
const [jobLocation,setJobLocation]=useState('');
|
||||
|
||||
const [passportOne,setPassportOne]=useState(null);
|
||||
const [passportTwo,setPassportTwo]=useState(null);
|
||||
const [passportThree,setPassportThree]=useState(null);
|
||||
const [passportFour,setPassportFour]=useState(null);
|
||||
|
||||
const [branchOptions,setBranchOptions]=useState([]);
|
||||
const [loading,setLoading]=useState(false);
|
||||
const [webVisible,setWebVisible]=useState(false);
|
||||
const [paymentUrl,setPaymentUrl]=useState('');
|
||||
const [newOrderId,setNewOrderId]=useState(null);
|
||||
|
||||
const cardStateOptions = getOptions('card_states');
|
||||
const cardTypeOptions = getOptions('card_types');
|
||||
const regionOptions = getOptions('regions');
|
||||
|
||||
// Prefill from user
|
||||
useEffect(()=>{
|
||||
if(user){
|
||||
if(user.passport_serie) setPassportSerie(user.passport_serie);
|
||||
if(user.passport_id) setPassportId(String(user.passport_id));
|
||||
if(user.region) setRegion(user.region);
|
||||
if(user.phone) setPhone(String(user.phone).slice(-8));
|
||||
if(user.name){ const parts=user.name.split(' '); setCustomerName(parts[0]); setCustomerSurname(parts[1]||''); }
|
||||
}
|
||||
},[user]);
|
||||
|
||||
useEffect(()=>{ (async()=>{ if(region){ const br=await getBranches(region); setBranchOptions(br.map(r=>({label:r.name,value:r.id}))); }else setBranchOptions([]); })(); },[region]);
|
||||
|
||||
const validate=()=>{
|
||||
return cardState && cardType && region && branchId && customerName && customerSurname && bornAt && passportSerie && passportId.trim() && passportGivenAt && passportGivenBy && bornPlace && jobLocation && passportAddress && realAddress && phone && passportOne && passportTwo && passportThree && passportFour;
|
||||
};
|
||||
|
||||
const handleSubmit=async()=>{
|
||||
if(!validate()){ Alert.alert('Ýalňyşlyk','Ähli zerur meýdançalar doldurylmaly'); return; }
|
||||
const fd=new FormData();
|
||||
fd.append('card_state_id',cardState);
|
||||
fd.append('card_type_id',cardType);
|
||||
fd.append('region',region);
|
||||
fd.append('branch_id',branchId);
|
||||
fd.append('customer_name',customerName);
|
||||
fd.append('customer_surname',customerSurname);
|
||||
if(customerPatro) fd.append('customer_patronic_name',customerPatro);
|
||||
if(oldSurname) fd.append('old_surname',oldSurname);
|
||||
fd.append('born_at',bornAt);
|
||||
fd.append('passport_serie',passportSerie);
|
||||
fd.append('passport_id',passportId.trim());
|
||||
fd.append('passport_given_at',passportGivenAt);
|
||||
fd.append('passport_given_by',passportGivenBy);
|
||||
fd.append('born_place',bornPlace);
|
||||
fd.append('job_location',jobLocation);
|
||||
fd.append('passport_address',passportAddress);
|
||||
fd.append('real_address',realAddress);
|
||||
fd.append('phone',parseInt(phone));
|
||||
if(phoneAdditional) fd.append('phone_additional',parseInt(phoneAdditional));
|
||||
fd.append('passport_one',{uri:passportOne,name:'p1.jpg',type:'image/jpeg'});
|
||||
fd.append('passport_two',{uri:passportTwo,name:'p2.jpg',type:'image/jpeg'});
|
||||
fd.append('passport_three',{uri:passportThree,name:'p3.jpg',type:'image/jpeg'});
|
||||
fd.append('passport_four',{uri:passportFour,name:'p4.jpg',type:'image/jpeg'});
|
||||
|
||||
setLoading(true);
|
||||
const res = await apiService.createCardOrder(fd);
|
||||
setLoading(false);
|
||||
if(res.success){
|
||||
const payment=res.data.payment;
|
||||
Alert.alert('Üstünlik',res.data.message||'Döredildi');
|
||||
// Try to get latest order id
|
||||
const list = await apiService.getCardOrders();
|
||||
if(list.success && Array.isArray(list.data) && list.data.length>0){ const maxOrder=list.data.reduce((a,b)=>a.id>b.id?a:b); setNewOrderId(maxOrder.id); }
|
||||
if(payment && payment.url){ setPaymentUrl(payment.url); setWebVisible(true);} else if(newOrderId){ navigation.replace('CardOrderDetails',{orderId:newOrderId}); }
|
||||
} else {
|
||||
Alert.alert('Ýalňyşlyk',res.error||'Ýalňyşlyk');
|
||||
}
|
||||
};
|
||||
|
||||
const webRef=useRef();
|
||||
const handleWebNav=state=>{
|
||||
if(state.url.includes('/online-payment-store')){
|
||||
setTimeout(()=>{ setWebVisible(false); if(newOrderId) navigation.replace('CardOrderDetails',{orderId:newOrderId}); },3000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar style="dark" />
|
||||
<ScrollView contentContainerStyle={{paddingHorizontal:24,paddingTop:40,paddingBottom:120}} showsVerticalScrollIndicator={false}>
|
||||
<TouchableOpacity style={styles.backBtn} onPress={()=>navigation.goBack()}><Ionicons name="arrow-back" size={24} color={COLORS.textPrimary}/></TouchableOpacity>
|
||||
<Text style={styles.title}>Täze kart sargyt</Text>
|
||||
|
||||
{/* Card */}
|
||||
<SelectInput label="*Kart çykarylmagynyň sebabi" value={cardState} onValueChange={setCardState} options={cardStateOptions} placeholder="Saýla" />
|
||||
<SelectInput label="*Kart görnüşi" value={cardType} onValueChange={setCardType} options={cardTypeOptions} placeholder="Saýla" />
|
||||
|
||||
{/* Location */}
|
||||
<Text style={styles.sectionTitle}>Lokasiýa</Text>
|
||||
<SelectInput label="*Welaýat" value={region} onValueChange={setRegion} options={regionOptions} placeholder="Saýla" />
|
||||
<SelectInput label="*Şahamça" value={branchId} onValueChange={setBranchId} options={branchOptions} placeholder="Saýla" />
|
||||
|
||||
{/* Personal */}
|
||||
<Text style={styles.sectionTitle}>Şahsy maglumatlar</Text>
|
||||
<Input label="*Ady" value={customerName} onChangeText={setCustomerName}/>
|
||||
<Input label="*Familiýasy" value={customerSurname} onChangeText={setCustomerSurname}/>
|
||||
<Input label="Atasynyň ady" value={customerPatro} onChangeText={setCustomerPatro}/>
|
||||
<Input label="Köne familiýa" value={oldSurname} onChangeText={setOldSurname}/>
|
||||
<DateInput label="*Doglan güni" value={bornAt} onChange={setBornAt}/>
|
||||
<Input label="*Telefon" value={phone} onChangeText={setPhone} keyboardType="numeric" maxLength={8}/>
|
||||
<Input label="Telefon goşmaça" value={phoneAdditional} onChangeText={setPhoneAdditional} keyboardType="numeric" maxLength={8}/>
|
||||
|
||||
<Input label="*Ýazgy edilen salgyňyz" value={passportAddress} onChangeText={setPassportAddress}/>
|
||||
<Input label="*Häzirki ýaşaýyş ýeri" value={realAddress} onChangeText={setRealAddress}/>
|
||||
<Input label="*Işleýän ýeriňiz we wezipäňiz" value={jobLocation} onChangeText={setJobLocation}/>
|
||||
|
||||
{/* Passport */}
|
||||
<Text style={styles.sectionTitle}>Pasport</Text>
|
||||
<SelectInput label="*Passport seriýasy" value={passportSerie} onValueChange={setPassportSerie} options={PASSPORT_SERIES.map(v=>({label:v,value:v}))} placeholder="Saýla" />
|
||||
<Input label="*Passport belgisi" value={passportId} onChangeText={setPassportId} keyboardType="numeric" />
|
||||
<DateInput label="*Passport berlen senesi" value={passportGivenAt} onChange={setPassportGivenAt}/>
|
||||
<Input label="*Kim tarapyndan berildi" value={passportGivenBy} onChangeText={setPassportGivenBy}/>
|
||||
<Input label="*Doglan ýeri (passport)" value={bornPlace} onChangeText={setBornPlace}/>
|
||||
|
||||
{/* Images */}
|
||||
<Text style={styles.sectionTitle}>Pasport faýllar</Text>
|
||||
<View style={{flexDirection:'row',flexWrap:'wrap',justifyContent:'space-between'}}>
|
||||
<ImageInput label="Pasport (sahypa 1) *" image={passportOne} onChange={setPassportOne}/>
|
||||
<ImageInput label="Pasport (2-3 sah.) *" image={passportTwo} onChange={setPassportTwo}/>
|
||||
<ImageInput label="Pasport (8-9 sah.) *" image={passportThree} onChange={setPassportThree}/>
|
||||
<ImageInput label="Pasport (32 sah.) *" image={passportFour} onChange={setPassportFour}/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.submitBtn} onPress={handleSubmit} disabled={loading}>
|
||||
{loading?<ActivityIndicator color={COLORS.white}/> : <Text style={styles.submitText}>Sargyt et</Text>}
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
|
||||
{/* Payment WebView */}
|
||||
<Modal visible={webVisible} onRequestClose={()=>setWebVisible(false)} animationType="slide">
|
||||
<SafeAreaView style={{flex:1}}>
|
||||
<View style={{flexDirection:'row',alignItems:'center',padding:12,backgroundColor:COLORS.primary}}>
|
||||
<TouchableOpacity onPress={()=>{setWebVisible(false); if(newOrderId) navigation.replace('CardOrderDetails',{orderId:newOrderId});}}><Ionicons name="close" size={24} color="#fff"/></TouchableOpacity>
|
||||
<Text style={{color:'#fff',fontWeight:'600',marginLeft:16}}>Töleg</Text>
|
||||
</View>
|
||||
{paymentUrl!=='' && <WebView ref={webRef} source={{uri:paymentUrl}} onNavigationStateChange={handleWebNav}/>}
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container:{flex:1,backgroundColor:COLORS.backgroundSecondary},
|
||||
backBtn:{marginBottom:24},
|
||||
title:{fontSize:24,fontWeight:'bold',color:COLORS.textPrimary,marginBottom:24},
|
||||
sectionTitle:{fontWeight:'700',fontSize:18,marginTop:24,marginBottom:12,color:COLORS.textPrimary},
|
||||
submitBtn:{marginTop:32,backgroundColor:COLORS.primary,paddingVertical:16,borderRadius:8,alignItems:'center'},
|
||||
submitText:{color:COLORS.white,fontSize:16,fontWeight:'600'},
|
||||
});
|
||||
|
||||
export default CreateCardOrderScreen;
|
||||
150
src/screens/Card/CreateCardPinOrderScreen.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 { useNavigation } from '@react-navigation/native';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import apiService from '../../services/apiService';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
import Input from '../../components/Input';
|
||||
import SelectInput from '../../components/SelectInput';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import DateInput from '../../components/DateInput';
|
||||
import ImageInput from '../../components/ImageInput';
|
||||
import { useBaseEnums } from '../../contexts/BaseEnumsContext';
|
||||
|
||||
const PASSPORT_SERIES = ['I-AS','I-MR','II-MR','I-AH','II-AH','I-LB','II-LB','I-BN','II-BN','I-DZ','II-DZ'];
|
||||
|
||||
const formatCardNumber = (val) => {
|
||||
const digits = String(val).replace(/[^0-9]/g,'').slice(0,16);
|
||||
return (digits.match(/.{1,4}/g)||[]).join(' ');
|
||||
};
|
||||
|
||||
const CreateCardPinOrderScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const { user } = useAuth();
|
||||
const { getOptions, getBranches } = useBaseEnums();
|
||||
|
||||
const [passportSerie,setPassportSerie]=useState('');
|
||||
const [passportId,setPassportId]=useState('');
|
||||
const [cardNumber,setCardNumber]=useState('');
|
||||
const [cardType,setCardType]=useState('');
|
||||
const [region,setRegion]=useState('');
|
||||
const [branchId,setBranchId]=useState('');
|
||||
const [customerName,setCustomerName]=useState('');
|
||||
const [customerSurname,setCustomerSurname]=useState('');
|
||||
const [customerPatro,setCustomerPatro]=useState('');
|
||||
const [bornAt,setBornAt]=useState('');
|
||||
const [phone,setPhone]=useState('');
|
||||
const [passportOne,setPassportOne]=useState(null);
|
||||
const [passportTwo,setPassportTwo]=useState(null);
|
||||
const [passportThree,setPassportThree]=useState(null);
|
||||
const [passportFour,setPassportFour]=useState(null);
|
||||
const [branchOptions,setBranchOptions]=useState([]);
|
||||
const [loading,setLoading]=useState(false);
|
||||
|
||||
const cardTypeOptions = getOptions('card_types');
|
||||
const regionOptions = getOptions('regions');
|
||||
|
||||
useEffect(()=>{
|
||||
if(user){
|
||||
if(user.passport_serie) setPassportSerie(user.passport_serie);
|
||||
if(user.passport_id) setPassportId(String(user.passport_id));
|
||||
if(user.card_number) setCardNumber(formatCardNumber(user.card_number));
|
||||
if(user.region) setRegion(user.region);
|
||||
if(user.phone) setPhone(String(user.phone).slice(-8));
|
||||
if(user.name){ const parts=user.name.split(' '); setCustomerName(parts[0]); setCustomerSurname(parts[1]||''); }
|
||||
}
|
||||
},[user]);
|
||||
|
||||
useEffect(()=>{(async()=>{
|
||||
if(region){ const b = await getBranches(region); setBranchOptions(b.map(br=>({label:br.name,value:br.id})));} else {setBranchOptions([]);} })();},[region]);
|
||||
|
||||
const handleCardNumberChange = (v)=> setCardNumber(formatCardNumber(v));
|
||||
|
||||
const handleSubmit = async()=>{
|
||||
if(!cardType||!cardNumber.trim()||!region||!branchId||!customerName||!customerSurname||!bornAt||!phone||!passportSerie||!passportId.trim()||!passportOne||!passportTwo||!passportThree||!passportFour){
|
||||
Alert.alert('Ýalňyşlyk','Ähli zerur meýdançalar doldurylmaly'); return;
|
||||
}
|
||||
|
||||
const payload = new FormData();
|
||||
payload.append('card_type_id', cardType);
|
||||
payload.append('card_number', cardNumber.replace(/\s+/g,''));
|
||||
payload.append('region', region);
|
||||
payload.append('branch_id', branchId);
|
||||
payload.append('customer_name', customerName);
|
||||
payload.append('customer_surname', customerSurname);
|
||||
if(customerPatro) payload.append('customer_patronic_name', customerPatro);
|
||||
payload.append('born_at', bornAt);
|
||||
payload.append('phone', parseInt(phone));
|
||||
payload.append('passport_serie', passportSerie);
|
||||
payload.append('passport_id', passportId.trim());
|
||||
payload.append('passport_one', { uri: passportOne, name: 'p1.jpg', type:'image/jpeg'});
|
||||
payload.append('passport_two', { uri: passportTwo, name: 'p2.jpg', type:'image/jpeg'});
|
||||
payload.append('passport_three', { uri: passportThree, name: 'p3.jpg', type:'image/jpeg'});
|
||||
payload.append('passport_four', { uri: passportFour, name: 'p4.jpg', type:'image/jpeg'});
|
||||
|
||||
setLoading(true);
|
||||
const res = await apiService.createCardPinOrder(payload);
|
||||
setLoading(false);
|
||||
if(res.success){
|
||||
Alert.alert('Üstünlik', res.message || 'Döredildi', [{ text:'OK', onPress:()=>navigation.goBack() }]);
|
||||
} else {
|
||||
Alert.alert('Ýalňyşlyk', res.error || 'Ýalňyşlyk ýüze çykdy');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar style="dark" />
|
||||
<ScrollView contentContainerStyle={{paddingHorizontal:24,paddingTop:40,paddingBottom:40}} showsVerticalScrollIndicator={false}>
|
||||
<TouchableOpacity style={styles.backBtn} onPress={()=>navigation.goBack()}>
|
||||
<Ionicons name="arrow-back" size={24} color={COLORS.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.title}>Täze sargyt</Text>
|
||||
|
||||
<SelectInput label="*Görnüşi" value={cardType} onValueChange={setCardType} options={cardTypeOptions} placeholder="Saýla" />
|
||||
<Input label="*Kart belgisi" placeholder="9934 6121 0000 0243" value={cardNumber} onChangeText={handleCardNumberChange} keyboardType="numeric" maxLength={19} autoCapitalize="none" autoCorrect={false}/>
|
||||
|
||||
{/* Location */}
|
||||
<Text style={styles.sectionTitle}>Lokasiýa</Text>
|
||||
<SelectInput label="*Welaýat" value={region} onValueChange={setRegion} options={regionOptions} placeholder="Saýla" />
|
||||
<SelectInput label="*Şahamça" value={branchId} onValueChange={setBranchId} options={branchOptions.map(b=>({label:b.label,value:b.value}))} placeholder="Saýla" />
|
||||
|
||||
{/* Personal */}
|
||||
<Text style={styles.sectionTitle}>Şahsy maglumatlar</Text>
|
||||
<Input label="*Ady" value={customerName} onChangeText={setCustomerName} />
|
||||
<Input label="*Familiýasy" value={customerSurname} onChangeText={setCustomerSurname} />
|
||||
<Input label="Atasynyň ady" value={customerPatro} onChangeText={setCustomerPatro} />
|
||||
<DateInput label="*Doglan güni" value={bornAt} onChange={setBornAt} />
|
||||
<Input label="*Telefon" value={phone} onChangeText={setPhone} keyboardType="numeric" maxLength={8}/>
|
||||
|
||||
{/* Passport */}
|
||||
<Text style={styles.sectionTitle}>Pasport</Text>
|
||||
<SelectInput label="*Passport seriýasy" value={passportSerie} onValueChange={setPassportSerie} options={PASSPORT_SERIES.map(v=>({label:v,value:v}))} placeholder="Saýla" />
|
||||
<Input label="*Passport belgisi" value={passportId} onChangeText={setPassportId} keyboardType="numeric" />
|
||||
<View style={{flexDirection:'row',flexWrap:'wrap',justifyContent:'space-between'}}>
|
||||
<ImageInput label="Pasport (sahypa 1) *" image={passportOne} onChange={setPassportOne} />
|
||||
<ImageInput label="Pasport (2-3 sah.) *" image={passportTwo} onChange={setPassportTwo} />
|
||||
<ImageInput label="Pasport (8-9 sah.) *" image={passportThree} onChange={setPassportThree} />
|
||||
<ImageInput label="Pasport (32 sah.) *" image={passportFour} onChange={setPassportFour} />
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.submitBtn} onPress={handleSubmit} disabled={loading}>
|
||||
{loading ? <ActivityIndicator color={COLORS.white}/> : <Text style={styles.submitText}>Ýatda sakla</Text>}
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container:{flex:1,backgroundColor:COLORS.backgroundSecondary},
|
||||
backBtn:{marginBottom:24},
|
||||
title:{fontSize:24,fontWeight:'bold',color:COLORS.textPrimary,marginBottom:24},
|
||||
sectionTitle:{fontWeight:'700',fontSize:18,marginTop:24,marginBottom:12,color:COLORS.textPrimary},
|
||||
submitBtn:{marginTop:32,backgroundColor:COLORS.primary,paddingVertical:16,borderRadius:8,alignItems:'center'},
|
||||
submitText:{color:COLORS.white,fontSize:16,fontWeight:'600'},
|
||||
});
|
||||
|
||||
export default CreateCardPinOrderScreen;
|
||||
160
src/screens/Card/CreateCardRequisiteOrderScreen.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 { useNavigation } from '@react-navigation/native';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import apiService from '../../services/apiService';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
import Input from '../../components/Input';
|
||||
import SelectInput from '../../components/SelectInput';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import DateInput from '../../components/DateInput';
|
||||
import ImageInput from '../../components/ImageInput';
|
||||
import { useBaseEnums } from '../../contexts/BaseEnumsContext';
|
||||
|
||||
const monthOptions = Array.from({ length: 12 }).map((_, i) => ({ label: String(i+1).padStart(2,'0'), value: String(i+1).padStart(2,'0') }));
|
||||
const yearOptions = Array.from({ length: 60 }).map((_, i) => ({ label: String(new Date().getFullYear()+i), value: String(new Date().getFullYear()+i) }));
|
||||
const PASSPORT_SERIES = ['I-AS','I-MR','II-MR','I-AH','II-AH','I-LB','II-LB','I-BN','II-BN','I-DZ','II-DZ'];
|
||||
|
||||
const formatCardNumber = (val) => {
|
||||
const digits = String(val).replace(/[^0-9]/g,'').slice(0,16);
|
||||
return (digits.match(/.{1,4}/g)||[]).join(' ');
|
||||
};
|
||||
|
||||
const CreateCardRequisiteOrderScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const { user } = useAuth();
|
||||
const [passportSerie,setPassportSerie]=useState('');
|
||||
const [passportId,setPassportId]=useState('');
|
||||
const [cardNumber,setCardNumber]=useState('');
|
||||
const [cardMonth,setCardMonth]=useState('');
|
||||
const [cardYear,setCardYear]=useState('');
|
||||
const [cardName,setCardName]=useState('');
|
||||
const [region,setRegion]=useState('');
|
||||
const [branchId,setBranchId]=useState('');
|
||||
const [customerName,setCustomerName]=useState('');
|
||||
const [customerSurname,setCustomerSurname]=useState('');
|
||||
const [customerPatro,setCustomerPatro]=useState('');
|
||||
const [bornAt,setBornAt]=useState('');
|
||||
const [phone,setPhone]=useState('');
|
||||
const [cardType,setCardType]=useState('');
|
||||
const [passportOne,setPassportOne]=useState(null);
|
||||
const [passportTwo,setPassportTwo]=useState(null);
|
||||
const [passportThree,setPassportThree]=useState(null);
|
||||
const [passportFour,setPassportFour]=useState(null);
|
||||
const { getOptions, getBranches } = useBaseEnums();
|
||||
const cardTypeOptions = getOptions('card_types');
|
||||
const regionOptions = getOptions('regions');
|
||||
const [branchOptions,setBranchOptions]=useState([]);
|
||||
const [loading,setLoading]=useState(false);
|
||||
|
||||
useEffect(()=>{
|
||||
if(user){
|
||||
if(user.passport_serie) setPassportSerie(user.passport_serie);
|
||||
if(user.passport_id) setPassportId(String(user.passport_id));
|
||||
if(user.card_number) setCardNumber(formatCardNumber(user.card_number));
|
||||
if(user.card_month) setCardMonth(String(user.card_month).padStart(2,'0'));
|
||||
if(user.card_year) setCardYear(String(user.card_year));
|
||||
if(user.card_name) setCardName(user.card_name);
|
||||
if(user.region) setRegion(user.region);
|
||||
if(user.phone) setPhone(String(user.phone).slice(-8));
|
||||
if(user.name){ const parts=user.name.split(' '); setCustomerName(parts[0]); setCustomerSurname(parts[1]||''); }
|
||||
}
|
||||
},[user]);
|
||||
|
||||
useEffect(()=>{(async()=>{
|
||||
if(region){ const b = await getBranches(region); setBranchOptions(b.map(br=>({label:br.name,value:br.id})));} else {setBranchOptions([]);} })();},[region]);
|
||||
|
||||
const handleCardNumberChange=(v)=> setCardNumber(formatCardNumber(v));
|
||||
|
||||
const handleSubmit=async()=>{
|
||||
if(!cardType||!cardNumber.trim()||!cardMonth||!cardYear||!region||!branchId||!customerName||!customerSurname||!bornAt||!phone||!passportSerie||!passportId.trim()||!passportOne||!passportTwo||!passportThree||!passportFour){
|
||||
Alert.alert('Ýalňyşlyk', 'Ähli zerur meýdançalar doldurylmaly'); return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const payload=new FormData();
|
||||
payload.append('card_type_id',cardType);
|
||||
payload.append('passport_serie',passportSerie);
|
||||
payload.append('passport_id',passportId.trim());
|
||||
payload.append('card_number',cardNumber.replace(/\s+/g,''));
|
||||
payload.append('card_month',cardMonth);
|
||||
payload.append('card_year',cardYear);
|
||||
payload.append('region',region);
|
||||
payload.append('branch_id',branchId);
|
||||
payload.append('customer_name',customerName);
|
||||
payload.append('customer_surname',customerSurname);
|
||||
if(customerPatro) payload.append('customer_patronic_name',customerPatro);
|
||||
payload.append('born_at',bornAt);
|
||||
payload.append('phone',parseInt(phone));
|
||||
payload.append('passport_one',{uri:passportOne,name:'p1.jpg',type:'image/jpeg'});
|
||||
payload.append('passport_two',{uri:passportTwo,name:'p2.jpg',type:'image/jpeg'});
|
||||
payload.append('passport_three',{uri:passportThree,name:'p3.jpg',type:'image/jpeg'});
|
||||
payload.append('passport_four',{uri:passportFour,name:'p4.jpg',type:'image/jpeg'});
|
||||
if(cardName) payload.append('card_name',cardName);
|
||||
const res = await apiService.createCardRequisiteOrder(payload);
|
||||
setLoading(false);
|
||||
if(res.success){
|
||||
Alert.alert('Success',res.message||'Created',[{text:'OK',onPress:()=>navigation.goBack()}]);
|
||||
}else{
|
||||
Alert.alert('Error',res.error||'Failed');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar style="dark" />
|
||||
<ScrollView contentContainerStyle={{paddingHorizontal:24,paddingTop:40,paddingBottom:40}}>
|
||||
<TouchableOpacity style={styles.backBtn} onPress={()=>navigation.goBack()}>
|
||||
<Ionicons name="arrow-back" size={24} color={COLORS.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.title}>Täze sargyt</Text>
|
||||
|
||||
<SelectInput label="*Görnüşi" value={cardType} onValueChange={setCardType} options={cardTypeOptions} placeholder="Saýla"/>
|
||||
<Input label="*Kart belgisi" placeholder="9934 6121 0000 0243" value={cardNumber} onChangeText={handleCardNumberChange} keyboardType="numeric" maxLength={19} autoCapitalize="none" autoCorrect={false}/>
|
||||
<SelectInput label="*Aý" value={cardMonth} onValueChange={setCardMonth} options={monthOptions} placeholder="Saýla" />
|
||||
<SelectInput label="*Ýyl" value={cardYear} onValueChange={setCardYear} options={yearOptions} placeholder="Saýla" />
|
||||
|
||||
{/* Location */}
|
||||
<Text style={{fontWeight:'700',fontSize:18,marginTop:24,marginBottom:12}}>Lokasiýa</Text>
|
||||
<SelectInput label="*Welaýat" value={region} onValueChange={setRegion} options={regionOptions} placeholder="Saýla" />
|
||||
<SelectInput label="*Şahamça" value={branchId} onValueChange={setBranchId} options={branchOptions.map(b=>({label:b.label,value:b.value}))} placeholder="Saýla" />
|
||||
|
||||
{/* Personal */}
|
||||
<Text style={{fontWeight:'700',fontSize:18,marginTop:24,marginBottom:12}}>Şahsy maglumatlar</Text>
|
||||
<Input label="*Ady" value={customerName} onChangeText={setCustomerName}/>
|
||||
<Input label="*Familiýasy" value={customerSurname} onChangeText={setCustomerSurname}/>
|
||||
<Input label="Atasynyň ady" value={customerPatro} onChangeText={setCustomerPatro}/>
|
||||
<DateInput label="*Doglan güni" value={bornAt} onChange={setBornAt} />
|
||||
<Input label="*Telefon" value={phone} onChangeText={setPhone} keyboardType="numeric" maxLength={8}/>
|
||||
|
||||
{/* Passport images */}
|
||||
<Text style={{fontWeight:'700',fontSize:18,marginTop:24,marginBottom:12}}>Pasport</Text>
|
||||
<SelectInput label="*Passport seriýasy" value={passportSerie} onValueChange={setPassportSerie} options={PASSPORT_SERIES.map(v=>({label:v,value:v}))} placeholder="Saýla"/>
|
||||
<Input label="*Passport belgisi" value={passportId} onChangeText={setPassportId} keyboardType="numeric" />
|
||||
<View style={{flexDirection:'row',flexWrap:'wrap',justifyContent:'space-between'}}>
|
||||
<ImageInput label="Pasport (sahypa 1) *" image={passportOne} onChange={setPassportOne} />
|
||||
<ImageInput label="Pasport (2-3 sah.) *" image={passportTwo} onChange={setPassportTwo} />
|
||||
<ImageInput label="Pasport (8-9 sah.) *" image={passportThree} onChange={setPassportThree} />
|
||||
<ImageInput label="Pasport (32 sah.) *" image={passportFour} onChange={setPassportFour} />
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.submitBtn} onPress={handleSubmit} disabled={loading}>
|
||||
{loading? <ActivityIndicator color={COLORS.white}/> : <Text style={styles.submitText}>Ýatda sakla</Text>}
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container:{flex:1,backgroundColor:COLORS.backgroundSecondary},
|
||||
backBtn:{marginBottom:24},
|
||||
title:{fontSize:24,fontWeight:'bold',color:COLORS.textPrimary,marginBottom:24},
|
||||
submitBtn:{marginTop:32,backgroundColor:COLORS.primary,paddingVertical:16,borderRadius:8,alignItems:'center'},
|
||||
submitText:{color:COLORS.white,fontSize:16,fontWeight:'600'},
|
||||
});
|
||||
|
||||
export default CreateCardRequisiteOrderScreen;
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { useNavigation } from '@react-navigation/native';
|
||||
import apiService from '../../services/apiService';
|
||||
@@ -21,6 +22,12 @@ const yearOptions = Array.from({ length: 60 }).map((_, i) => {
|
||||
|
||||
const PASSPORT_SERIES = ['I-AS','I-MR','II-MR','I-AH','II-AH','I-LB','II-LB','I-BN','II-BN','I-DZ','II-DZ'];
|
||||
|
||||
// Helper to format card number 9999 9999 9999 9999
|
||||
const formatCardNumber = (value) => {
|
||||
const digits = value.replace(/[^0-9]/g, '').slice(0, 16);
|
||||
return (digits.match(/.{1,4}/g) || []).join(' ');
|
||||
};
|
||||
|
||||
const CreateCardTransactionOrderScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const [cardNumber, setCardNumber] = useState('');
|
||||
@@ -35,16 +42,25 @@ const CreateCardTransactionOrderScreen = () => {
|
||||
if (user) {
|
||||
if (user.passport_serie) setPassportSerie(user.passport_serie);
|
||||
if (user.passport_id) setPassportId(String(user.passport_id));
|
||||
|
||||
if (user.card_number) setCardNumber(formatCardNumber(String(user.card_number)));
|
||||
if (user.card_month) setCardMonth(String(user.card_month).padStart(2,'0'));
|
||||
if (user.card_year) setCardYear(String(user.card_year));
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleCardNumberChange = (val) => {
|
||||
setCardNumber(formatCardNumber(val));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!cardNumber.trim() || !cardMonth || !cardYear || !passportSerie || !passportId.trim()) {
|
||||
Alert.alert('Error', 'All fields are required');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await apiService.createCardTransactionOrder(cardNumber.trim(), cardMonth, cardYear, passportSerie, passportId.trim());
|
||||
const rawCard = cardNumber.replace(/\s+/g, '').trim();
|
||||
const res = await apiService.createCardTransactionOrder(rawCard, cardMonth, cardYear, passportSerie, passportId.trim());
|
||||
setLoading(false);
|
||||
if (res.success) {
|
||||
Alert.alert('Success', res.message || 'Order created successfully', [
|
||||
@@ -85,10 +101,11 @@ const CreateCardTransactionOrderScreen = () => {
|
||||
label="*Kart belgisi"
|
||||
placeholder="9934..."
|
||||
value={cardNumber}
|
||||
onChangeText={setCardNumber}
|
||||
onChangeText={handleCardNumberChange}
|
||||
keyboardType="numeric"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
maxLength={19}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { useNavigation } from '@react-navigation/native';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
@@ -184,13 +185,18 @@ const CreateLoanOrderScreen = () => {
|
||||
if (
|
||||
!loanType || !loanAmount || !region || !branchId || !customerName || !customerSurname || !passportSerie || !passportId || !passportGivenAt || !passportGivenBy || !bornAt || !bornPlace || !phone || !phoneHome || !education || !marriageStatus || !passportAddress || !realAddress || !workCompany || !workCompanyAccNum || !workRegion || !workProvinceId || !workPosition || !workSalary || !workStartedAt || !passportOne || !passportTwo || !passportThree || !passportFour || !cardNumber || !cardName || !cardMonth || !cardYear || !guarantorName || !guarantorSurname || !guarantorCardNumber || !guarantorCardName || !guarantorCardMonth || !guarantorCardYear || !guarantorPassportSerie || !guarantorPassportId || (needsSecondGuarantor && ( !guarantor2Name || !guarantor2Surname || !guarantor2CardNumber || !guarantor2CardName || !guarantor2CardMonth || !guarantor2CardYear || !guarantor2PassportSerie || !guarantor2PassportId ))
|
||||
) {
|
||||
Alert.alert('Error', 'Fill all required fields');
|
||||
Alert.alert('Ýalňyşlyk', 'Ähli zerur meýdançalary dolduryň');
|
||||
return;
|
||||
}
|
||||
|
||||
if (passportId.length != 6) {
|
||||
Alert.alert('Ýalňyşlyk', 'Passport nomeri 6 san bolmaly');
|
||||
return;
|
||||
}
|
||||
|
||||
const rawCardNumber = cardNumber.replace(/[^0-9]/g, '');
|
||||
if (rawCardNumber.length !== 16) {
|
||||
Alert.alert('Error', 'Kart belgisi dogry dolduryň (16 sany rakam)');
|
||||
Alert.alert('Ýalňyşlyk', 'Kart belgisi dogry dolduryň (16 sany rakam)');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -290,9 +296,12 @@ const CreateLoanOrderScreen = () => {
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
console.log('[LoanOrder] API status:', response.status, response.ok);
|
||||
console.log('[LoanOrder] API response:', json);
|
||||
|
||||
res = { success: response.ok, ...(response.ok ? { message: json.message } : { error: json.message }) };
|
||||
} catch (e) {
|
||||
console.error('[LoanOrder] API error', e);
|
||||
@@ -300,9 +309,9 @@ const CreateLoanOrderScreen = () => {
|
||||
}
|
||||
setLoading(false);
|
||||
if (res.success) {
|
||||
Alert.alert('Success', res.message || 'Order created', [{ text: 'OK', onPress: () => navigation.goBack() }]);
|
||||
Alert.alert('Üstünlik', res.message || 'Order created', [{ text: 'OK', onPress: () => navigation.goBack() }]);
|
||||
} else {
|
||||
Alert.alert('Error', res.error || 'Could not create');
|
||||
Alert.alert('Ýalňyşlyk', res.error || 'Could not create');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -342,7 +351,9 @@ const CreateLoanOrderScreen = () => {
|
||||
<Input label={req('Häzirki ýaşaýyş ýeri')} placeholder="Kemine 100/200" value={realAddress} onChangeText={setRealAddress} error={submitted && !realAddress} />
|
||||
|
||||
<SelectInput label={req('Passport seriýasy')} value={passportSerie} options={passportSeriesOptions} onValueChange={setPassportSerie} placeholder="Saýla" error={submitted && !passportSerie} />
|
||||
<Input label={req('Passport nomeri')} placeholder="100999" value={passportId} onChangeText={setPassportId} keyboardType="numeric" error={submitted && !passportId} />
|
||||
|
||||
<Input label={req('Passport nomeri')} placeholder="100999" value={passportId} onChangeText={setPassportId} keyboardType="numeric" error={submitted && !passportId} maxLength={6} />
|
||||
|
||||
<DateInput label={req('Passport berlen senesi')} value={passportGivenAt} onChange={setPassportGivenAt} error={submitted && !passportGivenAt} />
|
||||
<Input label={req('Kim tarapyndan berildi')} placeholder="Ashgabat polisiýasy tarapyndan" value={passportGivenBy} onChangeText={setPassportGivenBy} error={submitted && !passportGivenBy} />
|
||||
<Input label={req('Doglan ýeri (passport)')} placeholder="Ashgabat" value={bornPlace} onChangeText={setBornPlace} error={submitted && !bornPlace} />
|
||||
@@ -350,10 +361,10 @@ const CreateLoanOrderScreen = () => {
|
||||
|
||||
<Input label={req('Telefon (+9936...)')} value={phone} onChangeText={setPhone} keyboardType="numeric" error={submitted && !phone} />
|
||||
<Input label="Telefon goşmaça" value={phoneAdditional} onChangeText={setPhoneAdditional} keyboardType="numeric" />
|
||||
<Input label={req('Öý telefony')} value={phoneHome} onChangeText={setPhoneHome} error={submitted && !phoneHome} />
|
||||
<Input label={req('Öý telefony')} value={phoneHome} onChangeText={setPhoneHome} keyboardType="numeric" error={submitted && !phoneHome} />
|
||||
|
||||
<Input label={req('Işleýän edaranyň/kärhananyň ady')} value={workCompany} onChangeText={setWorkCompany} error={submitted && !workCompany} />
|
||||
<Input label={req('Işgärler bölüminiň iş belgisi')} value={workCompanyAccNum} onChangeText={setWorkCompanyAccNum} error={submitted && !workCompanyAccNum} />
|
||||
<Input label={req('Işgärler bölüminiň iş belgisi')} value={workCompanyAccNum} onChangeText={setWorkCompanyAccNum} keyboardType="numeric" error={submitted && !workCompanyAccNum} />
|
||||
<SelectInput label={req('Işleýän welaýatyňyz')} value={workRegion} options={regionOptions} onValueChange={(val)=>{setWorkRegion(val); setWorkProvinceId('');}} placeholder="Saýla" error={submitted && !workRegion} />
|
||||
<SelectInput label={req('Işleýän etrabyňyz')} value={workProvinceId} options={workRegion && branchesByRegion[workRegion] ? branchesByRegion[workRegion].map(b=>({label:b.name,value:b.id})) : []} onValueChange={setWorkProvinceId} placeholder="Saýla" disabled={!workRegion} error={submitted && !workProvinceId} />
|
||||
<Input label={req('Wezipe')} value={workPosition} onChangeText={setWorkPosition} error={submitted && !workPosition} />
|
||||
@@ -388,7 +399,7 @@ const CreateLoanOrderScreen = () => {
|
||||
|
||||
{/* Guarantor passport */}
|
||||
<SelectInput label={req('Pasport seriýasy')} value={guarantorPassportSerie} options={passportSeriesOptions} onValueChange={setGuarantorPassportSerie} placeholder="Saýla" error={submitted && !guarantorPassportSerie} />
|
||||
<Input label={req('Pasport belgisi')} value={guarantorPassportId} onChangeText={setGuarantorPassportId} keyboardType="numeric" error={submitted && !guarantorPassportId} />
|
||||
<Input label={req('Pasport belgisi')} value={guarantorPassportId} onChangeText={setGuarantorPassportId} keyboardType="numeric" error={submitted && !guarantorPassportId} maxLength={6} />
|
||||
|
||||
{needsSecondGuarantor && (
|
||||
<>
|
||||
@@ -404,7 +415,7 @@ const CreateLoanOrderScreen = () => {
|
||||
<SelectInput label={req('Möhleti (ýyl)')} value={guarantor2CardYear} options={yearOptions} onValueChange={setGuarantor2CardYear} placeholder="Saýla" error={submitted && needsSecondGuarantor && !guarantor2CardYear} />
|
||||
|
||||
<SelectInput label={req('Pasport seriýasy')} value={guarantor2PassportSerie} options={passportSeriesOptions} onValueChange={setGuarantor2PassportSerie} placeholder="Saýla" error={submitted && needsSecondGuarantor && !guarantor2PassportSerie} />
|
||||
<Input label={req('Pasport belgisi')} value={guarantor2PassportId} onChangeText={setGuarantor2PassportId} keyboardType="numeric" error={submitted && needsSecondGuarantor && !guarantor2PassportId} />
|
||||
<Input label={req('Pasport belgisi')} value={guarantor2PassportId} onChangeText={setGuarantor2PassportId} keyboardType="numeric" error={submitted && needsSecondGuarantor && !guarantor2PassportId} maxLength={6} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { useNavigation } from '@react-navigation/native';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
|
||||
@@ -1,24 +1,39 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, SafeAreaView } from 'react-native';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 { useNavigation } from '@react-navigation/native';
|
||||
import apiService from '../../services/apiService';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
import Input from '../../components/Input';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import SelectInput from '../../components/SelectInput';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const CreateLoanRemainingOrderScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const [accountNumber, setAccountNumber] = useState('');
|
||||
const [passportSerie, setPassportSerie] = useState('');
|
||||
const [passportId, setPassportId] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { user } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
if (user.passport_serie) setPassportSerie(user.passport_serie);
|
||||
if (user.passport_id) setPassportId(String(user.passport_id));
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const PASSPORT_SERIES = ['I-AS','I-MR','II-MR','I-AH','II-AH','I-LB','II-LB','I-BN','II-BN','I-DZ','II-DZ'];
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (accountNumber.trim().length === 0) {
|
||||
Alert.alert('Error', 'Account number is required');
|
||||
if (accountNumber.trim().length === 0 || !passportSerie || passportId.trim().length === 0) {
|
||||
Alert.alert('Error', 'All fields are required');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await apiService.createLoanRemainingOrder(accountNumber.trim());
|
||||
const res = await apiService.createLoanRemainingOrder(accountNumber.trim(), passportSerie, passportId.trim());
|
||||
setLoading(false);
|
||||
if (res.success) {
|
||||
Alert.alert('Success', res.message || 'Order created successfully', [
|
||||
@@ -40,6 +55,22 @@ const CreateLoanRemainingOrderScreen = () => {
|
||||
|
||||
<Text style={styles.title}>Täze sargyt</Text>
|
||||
|
||||
<SelectInput
|
||||
label="*Passport seriýasy"
|
||||
value={passportSerie}
|
||||
onValueChange={setPassportSerie}
|
||||
options={PASSPORT_SERIES.map((v) => ({ label: v, value: v }))}
|
||||
placeholder="Saýla"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="*Passport belgisi"
|
||||
placeholder="123456"
|
||||
value={passportId}
|
||||
onChangeText={setPassportId}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Karz hasaby"
|
||||
placeholder="1420..."
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import apiService from '../../services/apiService';
|
||||
@@ -59,20 +60,20 @@ const LoanOrderDetailsScreen = () => {
|
||||
}, []);
|
||||
|
||||
const handleDelete = () => {
|
||||
Alert.alert('Confirm', 'Delete this order?', [
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{ text: 'Delete', style: 'destructive', onPress: deleteOrder },
|
||||
Alert.alert('Tassykla', 'Bu sargydy pozmak isleýärsiňizmi?', [
|
||||
{ text: 'Goýbolsun', style: 'cancel' },
|
||||
{ text: 'Poz', style: 'destructive', onPress: deleteOrder },
|
||||
]);
|
||||
};
|
||||
|
||||
const deleteOrder = async () => {
|
||||
const res = await apiService.deleteLoanOrder(orderId);
|
||||
if (res.success) {
|
||||
Alert.alert('Deleted', res.message || 'Order deleted', [
|
||||
{ text: 'OK', onPress: () => navigation.goBack() },
|
||||
Alert.alert('Pozuldy', res.message || 'Sargyt pozuldy', [
|
||||
{ text: 'Bolýar', onPress: () => navigation.goBack() },
|
||||
]);
|
||||
} else {
|
||||
Alert.alert('Error', res.error || 'Could not delete');
|
||||
Alert.alert('Ýalňyşlyk', res.error || 'Pozup bolmady');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -173,12 +174,11 @@ const LoanOrderDetailsScreen = () => {
|
||||
{/* Applicant card info */}
|
||||
{order.card_number && (
|
||||
<>
|
||||
<Text style={styles.sectionTitle}>Kartanyň maglumatlary (Arza beriji)</Text>
|
||||
<Text style={styles.sectionTitle}>Kart maglumatlary (Arza beriji)</Text>
|
||||
<View style={styles.detailCard}>
|
||||
{order.card_name && <DetailRow label="Kartanyň ady" value={order.card_name} />}
|
||||
<DetailRow label="Kartanyň belgisi" value={order.card_number} />
|
||||
{order.card_month && <DetailRow label="Karta – aý" value={order.card_month} />}
|
||||
{order.card_year && <DetailRow label="Karta – ýyl" value={order.card_year} showBorder={false} />}
|
||||
{order.card_name && <DetailRow label="Kartdaky ady" value={order.card_name} />}
|
||||
<DetailRow label="Kart belgisi" value={order.card_number} />
|
||||
{order.card_month && order.card_year && <DetailRow label="Kart möhleti" value={`${order.card_month}/${order.card_year}`} showBorder={false} />}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
@@ -189,11 +189,9 @@ const LoanOrderDetailsScreen = () => {
|
||||
<Text style={styles.sectionTitle}>Zamun (1)</Text>
|
||||
<View style={styles.detailCard}>
|
||||
<DetailRow label="Doly ady" value={guarantorFullName} />
|
||||
{order.guarantor_card_name && <DetailRow label="Kartanyň ady" value={order.guarantor_card_name} />}
|
||||
{order.guarantor_card_number && <DetailRow label="Kartanyň belgisi" value={order.guarantor_card_number} />}
|
||||
{order.guarantor_card_month && <DetailRow label="Karta – aý" value={order.guarantor_card_month} />}
|
||||
{order.guarantor_card_year && <DetailRow label="Karta – ýyl" value={order.guarantor_card_year} />}
|
||||
{order.guarantor_note && <DetailRow label="Bellik" value={order.guarantor_note} showBorder={false} />}
|
||||
{order.guarantor_card_name && <DetailRow label="Kartdaky ady" value={order.guarantor_card_name} />}
|
||||
{order.guarantor_card_number && <DetailRow label="Kart belgisi" value={order.guarantor_card_number} />}
|
||||
{order.guarantor_card_month && order.guarantor_card_year && <DetailRow label="Kart möhleti" value={`${order.guarantor_card_month}/${order.guarantor_card_year}`} showBorder={false} />}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
@@ -204,11 +202,9 @@ const LoanOrderDetailsScreen = () => {
|
||||
<Text style={styles.sectionTitle}>Zamun (2)</Text>
|
||||
<View style={styles.detailCard}>
|
||||
<DetailRow label="Doly ady" value={guarantor2FullName} />
|
||||
{order.guarantor_2_card_name && <DetailRow label="Kartanyň ady" value={order.guarantor_2_card_name} />}
|
||||
{order.guarantor_2_card_number && <DetailRow label="Kartanyň belgisi" value={order.guarantor_2_card_number} />}
|
||||
{order.guarantor_2_card_month && <DetailRow label="Karta – aý" value={order.guarantor_2_card_month} />}
|
||||
{order.guarantor_2_card_year && <DetailRow label="Karta – ýyl" value={order.guarantor_2_card_year} />}
|
||||
{order.guarantor_2_note && <DetailRow label="Bellik" value={order.guarantor_2_note} showBorder={false} />}
|
||||
{order.guarantor_2_card_name && <DetailRow label="Kartdaky ady" value={order.guarantor_2_card_name} />}
|
||||
{order.guarantor_2_card_number && <DetailRow label="Kart belgisi" value={order.guarantor_2_card_number} />}
|
||||
{order.guarantor_2_card_month && order.guarantor_2_card_year && <DetailRow label="Kart möhleti" value={`${order.guarantor_2_card_month}/${order.guarantor_2_card_year}`} showBorder={false} />}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import apiService from '../../services/apiService';
|
||||
@@ -47,6 +48,32 @@ const LoanOrdersScreen = () => {
|
||||
const amount = (item.loan_amount + ' TMT') || '-';
|
||||
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 (
|
||||
<TouchableOpacity style={styles.card} onPress={() => navigation.navigate('LoanOrderDetails', { orderId: item.id })}>
|
||||
<View style={styles.circle}>
|
||||
@@ -58,8 +85,11 @@ const LoanOrdersScreen = () => {
|
||||
<Text style={{ color: COLORS.textPrimary }}>{amount}</Text>
|
||||
</Text>
|
||||
|
||||
<Text style={styles.dateText}>{created}</Text>
|
||||
<Text style={styles.dateText}>Döredilen senesi: {created}</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => handleDelete(item.id)} style={styles.deleteButton}>
|
||||
<Ionicons name="trash" size={24} color={COLORS.error} />
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -89,7 +119,7 @@ const LoanOrdersScreen = () => {
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={orders.length === 0 && styles.emptyContainer}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
||||
ListEmptyComponent={<Text style={styles.emptyText}>No orders yet</Text>}
|
||||
ListEmptyComponent={<Text style={styles.emptyText}>Maglumat ýok</Text>}
|
||||
/>
|
||||
|
||||
<TouchableOpacity style={styles.fab} onPress={() => navigation.navigate('CreateLoanOrder')}>
|
||||
@@ -112,6 +142,7 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
circle: {
|
||||
width: 40,
|
||||
@@ -127,7 +158,7 @@ const styles = StyleSheet.create({
|
||||
passportText: { fontWeight: '700', color: COLORS.textPrimary, marginBottom: 4 },
|
||||
accountLabel: { color: COLORS.textSecondary, fontSize: 14 },
|
||||
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' },
|
||||
emptyText: { fontSize: 16, color: COLORS.textSecondary },
|
||||
fab: {
|
||||
@@ -142,6 +173,9 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
elevation: 4,
|
||||
},
|
||||
deleteButton: {
|
||||
padding: 8,
|
||||
},
|
||||
});
|
||||
|
||||
export default LoanOrdersScreen;
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import apiService from '../../services/apiService';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import apiService from '../../services/apiService';
|
||||
@@ -47,6 +48,32 @@ const LoanPaidOffLetterOrdersScreen = () => {
|
||||
const accountLine = `Karz hasaby:`;
|
||||
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 (
|
||||
<TouchableOpacity style={styles.card} onPress={() => navigation.navigate('LoanPaidOffLetterOrderDetails', { orderId: item.id })}>
|
||||
<View style={styles.circle}>
|
||||
@@ -58,6 +85,9 @@ const LoanPaidOffLetterOrdersScreen = () => {
|
||||
<Text style={styles.accountValue}>{item.loan_contract_number || '-'}</Text>
|
||||
<Text style={styles.dateText}>{created}</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => handleDelete(item.id)} style={styles.deleteButton}>
|
||||
<Ionicons name="trash" size={24} color={COLORS.error} />
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -110,6 +140,7 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
circle: {
|
||||
width: 40,
|
||||
@@ -140,6 +171,9 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
elevation: 4,
|
||||
},
|
||||
deleteButton: {
|
||||
padding: 8,
|
||||
},
|
||||
});
|
||||
|
||||
export default LoanPaidOffLetterOrdersScreen;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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 { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import apiService from '../../services/apiService';
|
||||
@@ -63,7 +64,7 @@ const LoanRemainingOrderDetailsScreen = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SafeAreaView style={styles.container}>
|
||||
<TouchableOpacity style={styles.backBtn} onPress={() => navigation.goBack()}>
|
||||
<Ionicons name="close" size={28} color={COLORS.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
@@ -82,7 +83,7 @@ const LoanRemainingOrderDetailsScreen = () => {
|
||||
<TouchableOpacity style={styles.deleteBtn} onPress={handleDelete}>
|
||||
<Text style={styles.deleteText}>Poz</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import apiService from '../../services/apiService';
|
||||
@@ -65,6 +66,32 @@ const LoanRemainingOrdersScreen = () => {
|
||||
const accountLabel = 'Karz hasaby:';
|
||||
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 (
|
||||
<TouchableOpacity style={styles.card} onPress={() => handleItemPress(item)}>
|
||||
<View style={styles.circle}>
|
||||
@@ -74,8 +101,11 @@ const LoanRemainingOrdersScreen = () => {
|
||||
<Text style={styles.passportText}>{passportLine}</Text>
|
||||
<Text style={styles.accountLabel}>{accountLabel}</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>
|
||||
<TouchableOpacity onPress={() => handleDelete(item.id)} style={styles.deleteButton}>
|
||||
<Ionicons name="trash" size={24} color={COLORS.error} />
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -104,7 +134,7 @@ const LoanRemainingOrdersScreen = () => {
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={orders.length === 0 && styles.emptyContainer}
|
||||
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 */}
|
||||
@@ -204,6 +234,7 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between', // Added to push delete button to the right
|
||||
},
|
||||
circle: {
|
||||
width: 40,
|
||||
@@ -318,6 +349,9 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '600',
|
||||
color: COLORS.textPrimary,
|
||||
},
|
||||
deleteButton: {
|
||||
padding: 8,
|
||||
},
|
||||
});
|
||||
|
||||
export default LoanRemainingOrdersScreen;
|
||||
@@ -3,22 +3,120 @@ import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
RefreshControl,
|
||||
FlatList,
|
||||
} from 'react-native';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
import MetricCard from '../../components/MetricCard';
|
||||
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 { user, logout } = useAuth();
|
||||
const insets = useSafeAreaInsets();
|
||||
const [metrics, setMetrics] = useState(null);
|
||||
const [loadingMetrics, setLoadingMetrics] = useState(true);
|
||||
const [cardBalance, setCardBalance] = useState(null);
|
||||
const [loadingCardBalance, setLoadingCardBalance] = useState(true);
|
||||
const [cardBalanceError, setCardBalanceError] = useState(null);
|
||||
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;
|
||||
|
||||
// (Optional) Add helpers here if needed in the future
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMetrics = async () => {
|
||||
@@ -39,13 +137,130 @@ const HomeScreen = () => {
|
||||
fetchMetrics();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCardData = async () => {
|
||||
// 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) {
|
||||
setLoadingCardBalance(false);
|
||||
setLoadingTransactions(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingCardBalance(true);
|
||||
setLoadingTransactions(true);
|
||||
|
||||
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:', res.error);
|
||||
setCardBalanceError(res.error || 'Error');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error fetching card balance:', e);
|
||||
setCardBalanceError('Error fetching balance');
|
||||
} finally {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCardData();
|
||||
}, [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 (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.container}>
|
||||
<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 */}
|
||||
<View style={styles.header}>
|
||||
<View style={[styles.header, { paddingTop: insets.top + 16 }]}>
|
||||
<View>
|
||||
<Text style={styles.greeting}>Salam,</Text>
|
||||
<Text style={styles.userName}>{user?.name || 'Ulanyjy'}</Text>
|
||||
@@ -54,7 +269,7 @@ const HomeScreen = () => {
|
||||
|
||||
{/* Metrics */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Metrics</Text>
|
||||
<Text style={styles.sectionTitle}>Görkezijiler</Text>
|
||||
{loadingMetrics ? (
|
||||
<ActivityIndicator color={COLORS.primary} style={{ marginTop: 16 }} />
|
||||
) : (
|
||||
@@ -67,21 +282,46 @@ const HomeScreen = () => {
|
||||
</View>
|
||||
|
||||
{/* Balance Card */}
|
||||
<View style={styles.balanceCard}>
|
||||
<View style={styles.balanceHeader}>
|
||||
<Text style={styles.balanceLabel}>Jemi balans</Text>
|
||||
<TouchableOpacity>
|
||||
<Ionicons name="eye" size={20} color={COLORS.white} />
|
||||
</TouchableOpacity>
|
||||
{showBalanceCard && (
|
||||
<View style={styles.balanceCard}>
|
||||
<View style={styles.balanceHeader}>
|
||||
<Text style={styles.balanceLabel}>Jemi balans</Text>
|
||||
<TouchableOpacity onPress={() => setIsBalanceVisible((prev) => !prev)}>
|
||||
<Ionicons name={isBalanceVisible ? 'eye' : 'eye-off'} size={20} color={COLORS.white} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={styles.balanceAmount}>
|
||||
{isBalanceVisible ? `${cardBalance} TMT` : '•••••'}
|
||||
</Text>
|
||||
<View style={styles.balanceFooter}>
|
||||
<Text style={styles.accountNumber}>
|
||||
{user?.card_number ? `Hasap: ****${String(user.card_number).replace(/[^0-9]/g, '').slice(-4)}` : 'Hasap: ----'}
|
||||
</Text>
|
||||
<View style={styles.cardChip} />
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.balanceAmount}>1,250.00 TMT</Text>
|
||||
<View style={styles.balanceFooter}>
|
||||
<Text style={styles.accountNumber}>Hasap: ****1234</Text>
|
||||
<View style={styles.cardChip} />
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -98,7 +338,6 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 24,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 24,
|
||||
backgroundColor: COLORS.white,
|
||||
marginBottom: 16,
|
||||
@@ -172,16 +411,16 @@ const styles = StyleSheet.create({
|
||||
padding: 20,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: COLORS.textPrimary,
|
||||
marginBottom: 16,
|
||||
},
|
||||
sectionSubtitle: {
|
||||
fontSize: 14,
|
||||
color: COLORS.textSecondary,
|
||||
},
|
||||
seeAllText: {
|
||||
fontSize: 14,
|
||||
@@ -209,58 +448,6 @@ const styles = StyleSheet.create({
|
||||
color: COLORS.textSecondary,
|
||||
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: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
@@ -4,16 +4,17 @@ import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { COLORS } from '../../constants/colors';
|
||||
|
||||
const MenuScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const menuSections = [
|
||||
{
|
||||
@@ -34,13 +35,13 @@ const MenuScreen = () => {
|
||||
{ id: 8, title: 'Kart pin bukjalar', icon: 'key', description: 'Pin bukjalar' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Halkara tölegler',
|
||||
items: [
|
||||
{ id: 9, title: 'Visa/Master tölegleri (talyplar üçin)', icon: 'logo-usd', description: 'Visa/Master' },
|
||||
{ id: 10, title: 'Sber tölegler (talyplar üçin)', icon: 'globe', description: 'Sber tölegler' },
|
||||
],
|
||||
},
|
||||
// {
|
||||
// title: 'Halkara tölegler',
|
||||
// items: [
|
||||
// { id: 9, title: 'Visa/Master tölegleri (talyplar üçin)', icon: 'logo-usd', description: 'Visa/Master' },
|
||||
// { id: 10, title: 'Sber tölegler (talyplar üçin)', icon: 'globe', description: 'Sber tölegler' },
|
||||
// ],
|
||||
// },
|
||||
];
|
||||
|
||||
const handleMenuItemPress = (item) => {
|
||||
@@ -50,18 +51,26 @@ const MenuScreen = () => {
|
||||
navigation.navigate('LoanRemainingOrders');
|
||||
} else if (item.id === 3) {
|
||||
navigation.navigate('LoanPaidOffLetterOrders');
|
||||
} else if (item.id === 4) {
|
||||
navigation.navigate('CardOrders');
|
||||
} else if (item.id === 5) {
|
||||
navigation.navigate('CardTransactionOrders');
|
||||
} else if (item.id === 6) {
|
||||
navigation.navigate('CardRequisiteOrders');
|
||||
} else if (item.id === 7) {
|
||||
navigation.navigate('CardBalanceOrders');
|
||||
} else if (item.id === 8) {
|
||||
navigation.navigate('CardPinOrders');
|
||||
} else {
|
||||
console.log('Menu item pressed:', item.title);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.container}>
|
||||
<StatusBar style="dark" />
|
||||
|
||||
<View style={styles.header}>
|
||||
<View style={[styles.header, { paddingTop: insets.top + 16 }]}>
|
||||
<Text style={styles.headerTitle}>Hyzmatlar</Text>
|
||||
</View>
|
||||
|
||||
@@ -97,7 +106,7 @@ const MenuScreen = () => {
|
||||
|
||||
<View style={styles.bottomSpacing} />
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -108,7 +117,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
header: {
|
||||
paddingHorizontal: 24,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 24,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.gray[200],
|
||||
|
||||
@@ -3,15 +3,16 @@ import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
RefreshControl,
|
||||
Linking,
|
||||
} from 'react-native';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import apiService from '../../services/apiService';
|
||||
import EditProfileModal from '../../components/EditProfileModal';
|
||||
@@ -19,6 +20,7 @@ import { COLORS } from '../../constants/colors';
|
||||
|
||||
const ProfileScreen = () => {
|
||||
const { user, logout, fetchUserProfile } = useAuth();
|
||||
const insets = useSafeAreaInsets();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [profileData, setProfileData] = useState(null);
|
||||
@@ -59,28 +61,29 @@ const ProfileScreen = () => {
|
||||
{ id: 1, title: 'Şahsy maglumatlar', icon: 'person', hasArrow: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Sazlamalar',
|
||||
items: [
|
||||
{ id: 4, title: 'Bildirişler', icon: 'notifications', hasArrow: true },
|
||||
{ id: 6, title: 'Dil', icon: 'language', value: 'Türkmençe', hasArrow: true },
|
||||
{ id: 7, title: 'Tema', icon: 'color-palette', value: 'Ýeňil', hasArrow: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Kömek',
|
||||
items: [
|
||||
{ id: 8, title: 'Kömek merkezi', icon: 'help-circle', hasArrow: true },
|
||||
{ id: 9, title: 'Habarlaş', icon: 'chatbubble', hasArrow: true },
|
||||
{ id: 10, title: 'Baha ber', icon: 'star', hasArrow: true },
|
||||
],
|
||||
},
|
||||
// {
|
||||
// title: 'Sazlamalar',
|
||||
// items: [
|
||||
// { id: 4, title: 'Bildirişler', icon: 'notifications', hasArrow: true },
|
||||
// { id: 6, title: 'Dil', icon: 'language', value: 'Türkmençe', hasArrow: true },
|
||||
// { id: 7, title: 'Tema', icon: 'color-palette', value: 'Ýeňil', hasArrow: true },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// title: 'Kömek',
|
||||
// items: [
|
||||
// { id: 8, title: 'Kömek merkezi', icon: 'help-circle', hasArrow: true },
|
||||
// { id: 9, title: 'Habarlaş', icon: 'chatbubble', hasArrow: true },
|
||||
// { id: 10, title: 'Baha ber', icon: 'star', hasArrow: true },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
title: 'Goşmaça',
|
||||
items: [
|
||||
{ id: 11, title: 'Ulanmak düzgünleri', icon: 'document-text', 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: 11, title: 'Ulanmak düzgünleri', icon: 'document-text', 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: false },
|
||||
{ id: 14, title: 'Hasaby poz', icon: 'trash', hasArrow: false, danger: true },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -102,8 +105,14 @@ const ProfileScreen = () => {
|
||||
case 5: // Security
|
||||
handleSecuritySettings();
|
||||
break;
|
||||
case 13: // About App
|
||||
// Alert.alert('Üns beriň', 'Programma barada maglumatlar açylýar...');
|
||||
break;
|
||||
case 14: // Delete Account
|
||||
handleDeleteAccount();
|
||||
break;
|
||||
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 = () => {
|
||||
Alert.alert('Bildirişler', 'Bildiriş sazlamalary sahypasy açylýar...');
|
||||
};
|
||||
@@ -237,10 +280,10 @@ const ProfileScreen = () => {
|
||||
const currentUser = profileData || user;
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.container}>
|
||||
<StatusBar style="dark" />
|
||||
|
||||
<View style={styles.header}>
|
||||
<View style={[styles.header, { paddingTop: insets.top + 16 }]}>
|
||||
<Text style={styles.headerTitle}>Profil</Text>
|
||||
</View>
|
||||
|
||||
@@ -294,16 +337,16 @@ const ProfileScreen = () => {
|
||||
>
|
||||
<View style={styles.profileItemLeft}>
|
||||
<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>
|
||||
<Text style={styles.profileItemTitle}>{item.title}</Text>
|
||||
<Text style={item.danger ? styles.profileItemTitleDanger : styles.profileItemTitle}>{item.title}</Text>
|
||||
</View>
|
||||
<View style={styles.profileItemRight}>
|
||||
{item.value && (
|
||||
<Text style={styles.profileItemValue}>{item.value}</Text>
|
||||
<Text style={item.danger ? styles.profileItemValueDanger : styles.profileItemValue}>{item.value}</Text>
|
||||
)}
|
||||
{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>
|
||||
</TouchableOpacity>
|
||||
@@ -321,6 +364,21 @@ const ProfileScreen = () => {
|
||||
</View>
|
||||
|
||||
<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>
|
||||
|
||||
{/* Edit Profile Modal */}
|
||||
@@ -331,7 +389,7 @@ const ProfileScreen = () => {
|
||||
initialData={profileData || user}
|
||||
isLoading={isUpdatingProfile}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -343,7 +401,6 @@ const styles = StyleSheet.create({
|
||||
header: {
|
||||
backgroundColor: COLORS.white,
|
||||
paddingHorizontal: 24,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 24,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.gray[200],
|
||||
@@ -485,6 +542,24 @@ const styles = StyleSheet.create({
|
||||
bottomSpacing: {
|
||||
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;
|
||||
@@ -67,6 +67,37 @@ class ApiService {
|
||||
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
|
||||
async transferMoney(data) {
|
||||
return authService.transferMoney(data);
|
||||
@@ -106,9 +137,9 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async createLoanRemainingOrder(accountNumber) {
|
||||
async createLoanRemainingOrder(accountNumber, passportSerie = null, passportId = null) {
|
||||
try {
|
||||
const response = await authService.createLoanRemainingOrder(accountNumber);
|
||||
const response = await authService.createLoanRemainingOrder(accountNumber, passportSerie, passportId);
|
||||
return { success: true, message: response.message };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
@@ -124,9 +155,9 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async updateLoanRemainingOrder(orderId, accountNumber) {
|
||||
async updateLoanRemainingOrder(orderId, accountNumber, passportSerie = null, passportId = null) {
|
||||
try {
|
||||
const response = await authService.updateLoanRemainingOrder(orderId, accountNumber);
|
||||
const response = await authService.updateLoanRemainingOrder(orderId, accountNumber, passportSerie, passportId);
|
||||
return { success: true, message: response.message };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
@@ -165,6 +196,29 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
// Quick card balance check
|
||||
async getCardBalanceQuickCheck(cardNumber = null, cardMonth = null, cardYear = null, passportSerie = null, passportId = null) {
|
||||
try {
|
||||
let raw = await authService.getCardBalanceQuickCheck(cardNumber, cardMonth, cardYear, passportSerie, passportId);
|
||||
|
||||
// Handle text response that is JSON-stringified
|
||||
if (typeof raw === 'string') {
|
||||
try { raw = JSON.parse(raw); } catch (_) {}
|
||||
}
|
||||
|
||||
// When API returns { status: false, message: '...' }
|
||||
if (raw && raw.status === false) {
|
||||
return { success: false, error: raw.message || 'Failed', data: raw };
|
||||
}
|
||||
|
||||
// On success, data may be inside raw.data or raw itself
|
||||
const data = raw?.data ?? raw;
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Loan Paid-Off Letter Orders
|
||||
// ================================
|
||||
@@ -345,6 +399,202 @@ class ApiService {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Card Balance Orders (Kart galyndylary)
|
||||
// ================================
|
||||
|
||||
async getCardBalanceOrders() {
|
||||
try {
|
||||
const data = await authService.getCardBalanceOrders();
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async createCardBalanceOrder(cardNumber, cardMonth, cardYear, passportSerie = null, passportId = null) {
|
||||
try {
|
||||
const response = await authService.createCardBalanceOrder(cardNumber, cardMonth, cardYear, passportSerie, passportId);
|
||||
return { success: true, message: response.message };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async getCardBalanceOrder(orderId) {
|
||||
try {
|
||||
const data = await authService.getCardBalanceOrder(orderId);
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async updateCardBalanceOrder(orderId, cardNumber, cardMonth, cardYear, passportSerie = null, passportId = null) {
|
||||
try {
|
||||
const response = await authService.updateCardBalanceOrder(orderId, cardNumber, cardMonth, cardYear, passportSerie, passportId);
|
||||
return { success: true, message: response.message };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCardBalanceOrder(orderId) {
|
||||
try {
|
||||
const response = await authService.deleteCardBalanceOrder(orderId);
|
||||
return { success: true, message: response.message };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async downloadCardBalances(orderId) {
|
||||
try {
|
||||
const response = await authService.downloadCardBalances(orderId);
|
||||
return { success: true, data: response };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Card Requisites Orders (Kart rekwizitler)
|
||||
// ================================
|
||||
|
||||
async getCardRequisiteOrders() {
|
||||
try {
|
||||
const data = await authService.getCardRequisiteOrders();
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async createCardRequisiteOrder(payload) {
|
||||
try {
|
||||
const response = await authService.createCardRequisiteOrder(payload);
|
||||
return { success: true, message: response.message };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async getCardRequisiteOrder(orderId) {
|
||||
try {
|
||||
const data = await authService.getCardRequisiteOrder(orderId);
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async updateCardRequisiteOrder(orderId, payload) {
|
||||
try {
|
||||
const response = await authService.updateCardRequisiteOrder(orderId, payload);
|
||||
return { success: true, message: response.message };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCardRequisiteOrder(orderId) {
|
||||
try {
|
||||
const response = await authService.deleteCardRequisiteOrder(orderId);
|
||||
return { success: true, message: response.message };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async downloadCardRequisites(orderId) {
|
||||
try {
|
||||
const response = await authService.downloadCardRequisites(orderId);
|
||||
return { success: true, data: response };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Card Pin Orders (Kart pin bukjalar)
|
||||
// ================================
|
||||
|
||||
async getCardPinOrders() {
|
||||
try {
|
||||
const data = await authService.getCardPinOrders();
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async createCardPinOrder(payload) {
|
||||
try {
|
||||
const response = await authService.createCardPinOrder(payload);
|
||||
return { success: true, message: response.message };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async getCardPinOrder(orderId) {
|
||||
try {
|
||||
const data = await authService.getCardPinOrder(orderId);
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCardPinOrder(orderId) {
|
||||
try {
|
||||
const response = await authService.deleteCardPinOrder(orderId);
|
||||
return { success: true, message: response.message };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Card Orders (Täze kart)
|
||||
// ================================
|
||||
|
||||
async getCardOrders() {
|
||||
try {
|
||||
const data = await authService.getCardOrders();
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async createCardOrder(payload) {
|
||||
try {
|
||||
const response = await authService.createCardOrder(payload);
|
||||
return { success: true, data: response };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async getCardOrder(orderId) {
|
||||
try {
|
||||
const data = await authService.getCardOrder(orderId);
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCardOrder(orderId) {
|
||||
try {
|
||||
const response = await authService.deleteCardOrder(orderId);
|
||||
return { success: true, message: response.message };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ApiService();
|
||||
@@ -15,9 +15,17 @@ class AuthService {
|
||||
const requestId = Math.random().toString(36).substr(2, 9);
|
||||
|
||||
try {
|
||||
const headers = {
|
||||
...API_CONFIG.HEADERS,
|
||||
};
|
||||
const headers = { ...API_CONFIG.HEADERS };
|
||||
|
||||
// Determine body and adjust headers for FormData
|
||||
let bodyToSend;
|
||||
if (data instanceof FormData) {
|
||||
bodyToSend = data;
|
||||
// Let fetch set correct Content-Type with boundary
|
||||
if (headers['Content-Type']) delete headers['Content-Type'];
|
||||
} else {
|
||||
bodyToSend = data ? JSON.stringify(data) : undefined;
|
||||
}
|
||||
|
||||
// Auto-include token for authenticated requests
|
||||
if (requiresAuth) {
|
||||
@@ -45,7 +53,7 @@ class AuthService {
|
||||
const response = await fetch(fullUrl, {
|
||||
method,
|
||||
headers,
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
body: bodyToSend,
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
@@ -154,6 +162,10 @@ class AuthService {
|
||||
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) {
|
||||
return this.makeRequest('/user/change-password', {
|
||||
current_password: currentPassword,
|
||||
@@ -162,7 +174,7 @@ class AuthService {
|
||||
}
|
||||
|
||||
async deleteAccount() {
|
||||
return this.makeRequest('/user/delete-account', null, true, 'DELETE');
|
||||
return this.makeRequest(API_CONFIG.ENDPOINTS.AUTH.DELETE_USER, null, true, 'POST');
|
||||
}
|
||||
|
||||
// ================================
|
||||
@@ -184,15 +196,21 @@ class AuthService {
|
||||
return this.makeRequest('/loan-remaining-order', null, true, 'GET');
|
||||
}
|
||||
|
||||
// CREATE order (requires only account number – passport details are fetched from user profile)
|
||||
async createLoanRemainingOrder(accountNumber) {
|
||||
// CREATE order (passport can be supplied or fetched from profile)
|
||||
async createLoanRemainingOrder(accountNumber, passportSerie = null, passportId = null) {
|
||||
let serie = passportSerie;
|
||||
let pid = passportId;
|
||||
if (!serie || !pid) {
|
||||
const user = await this.getStoredUser();
|
||||
if (!user?.passport_serie || !user?.passport_id) {
|
||||
throw new Error('Passport details are missing from profile');
|
||||
serie = serie || user?.passport_serie;
|
||||
pid = pid || user?.passport_id;
|
||||
}
|
||||
if (!serie || !pid) {
|
||||
throw new Error('Passport details are missing');
|
||||
}
|
||||
const payload = {
|
||||
passport_serie: user.passport_serie,
|
||||
passport_id: user.passport_id,
|
||||
passport_serie: serie,
|
||||
passport_id: pid,
|
||||
account_number: accountNumber,
|
||||
};
|
||||
return this.makeRequest('/loan-remaining-order', payload, true, 'POST');
|
||||
@@ -204,14 +222,20 @@ class AuthService {
|
||||
}
|
||||
|
||||
// UPDATE order (only account number can change; passport details stay the same)
|
||||
async updateLoanRemainingOrder(orderId, accountNumber) {
|
||||
async updateLoanRemainingOrder(orderId, accountNumber, passportSerie = null, passportId = null) {
|
||||
let serie = passportSerie;
|
||||
let pid = passportId;
|
||||
if (!serie || !pid) {
|
||||
const user = await this.getStoredUser();
|
||||
if (!user?.passport_serie || !user?.passport_id) {
|
||||
throw new Error('Passport details are missing from profile');
|
||||
serie = serie || user?.passport_serie;
|
||||
pid = pid || user?.passport_id;
|
||||
}
|
||||
if (!serie || !pid) {
|
||||
throw new Error('Passport details are missing');
|
||||
}
|
||||
const payload = {
|
||||
passport_serie: user.passport_serie,
|
||||
passport_id: user.passport_id,
|
||||
passport_serie: serie,
|
||||
passport_id: pid,
|
||||
account_number: accountNumber,
|
||||
};
|
||||
return this.makeRequest(`/loan-remaining-order/${orderId}`, payload, true, 'POST');
|
||||
@@ -370,6 +394,211 @@ class AuthService {
|
||||
const query = `start_date=${encodeURIComponent(startDate)}&end_date=${encodeURIComponent(endDate)}`;
|
||||
return this.makeRequest(`/card-transactions-download/${orderId}?${query}`, null, true, 'GET');
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Card balance quick check (Kart galyndysy)
|
||||
// ================================
|
||||
|
||||
async getCardBalanceQuickCheck(cardNumber = null, cardMonth = null, cardYear = null, passportSerie = null, passportId = null) {
|
||||
// Fallback to stored user profile for missing fields
|
||||
let serie = passportSerie;
|
||||
let pid = passportId;
|
||||
let cNumber = cardNumber;
|
||||
let cMonth = cardMonth;
|
||||
let cYear = cardYear;
|
||||
|
||||
if (!serie || !pid || !cNumber || !cMonth || !cYear) {
|
||||
const user = await this.getStoredUser();
|
||||
serie = serie || user?.passport_serie;
|
||||
pid = pid || user?.passport_id;
|
||||
cNumber = cNumber || user?.card_number;
|
||||
cMonth = cMonth || user?.card_month;
|
||||
cYear = cYear || user?.card_year;
|
||||
}
|
||||
|
||||
if (!serie || !pid || !cNumber || !cMonth || !cYear) {
|
||||
throw new Error('Card or passport details are missing');
|
||||
}
|
||||
|
||||
// Ensure values are properly formatted
|
||||
const plainCardNumber = String(cNumber).replace(/[^0-9]/g, '').slice(0, 16);
|
||||
const monthStr = String(cMonth).padStart(2, '0');
|
||||
|
||||
const payload = {
|
||||
passport_serie: serie,
|
||||
passport_id: pid,
|
||||
card_number: plainCardNumber,
|
||||
card_month: monthStr,
|
||||
card_year: String(cYear),
|
||||
};
|
||||
|
||||
// POST request – authenticated (token header will be included if available)
|
||||
return this.makeRequest('/card-balance-quick-check', payload, true, 'POST');
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Card Balance Orders (Kart galyndylary)
|
||||
// ================================
|
||||
|
||||
// LIST
|
||||
async getCardBalanceOrders() {
|
||||
return this.makeRequest('/card-balances', null, true, 'GET');
|
||||
}
|
||||
|
||||
// CREATE
|
||||
async createCardBalanceOrder(cardNumber, cardMonth, cardYear, passportSerie = null, passportId = null) {
|
||||
let serie = passportSerie;
|
||||
let pid = passportId;
|
||||
if (!serie || !pid) {
|
||||
const user = await this.getStoredUser();
|
||||
serie = serie || user?.passport_serie;
|
||||
pid = pid || user?.passport_id;
|
||||
}
|
||||
if (!serie || !pid) {
|
||||
throw new Error('Passport details are missing');
|
||||
}
|
||||
const payload = {
|
||||
passport_serie: serie,
|
||||
passport_id: pid,
|
||||
card_number: cardNumber,
|
||||
card_month: cardMonth,
|
||||
card_year: cardYear,
|
||||
};
|
||||
return this.makeRequest('/card-balances', payload, true, 'POST');
|
||||
}
|
||||
|
||||
// SHOW
|
||||
async getCardBalanceOrder(orderId) {
|
||||
return this.makeRequest(`/card-balances/${orderId}`, null, true, 'GET');
|
||||
}
|
||||
|
||||
// UPDATE
|
||||
async updateCardBalanceOrder(orderId, cardNumber, cardMonth, cardYear, passportSerie = null, passportId = null) {
|
||||
let serie = passportSerie;
|
||||
let pid = passportId;
|
||||
if (!serie || !pid) {
|
||||
const user = await this.getStoredUser();
|
||||
serie = serie || user?.passport_serie;
|
||||
pid = pid || user?.passport_id;
|
||||
}
|
||||
if (!serie || !pid) {
|
||||
throw new Error('Passport details are missing');
|
||||
}
|
||||
const payload = {
|
||||
passport_serie: serie,
|
||||
passport_id: pid,
|
||||
card_number: cardNumber,
|
||||
card_month: cardMonth,
|
||||
card_year: cardYear,
|
||||
};
|
||||
return this.makeRequest(`/card-balances/${orderId}`, payload, true, 'POST');
|
||||
}
|
||||
|
||||
// DELETE
|
||||
async deleteCardBalanceOrder(orderId) {
|
||||
return this.makeRequest(`/card-balances/${orderId}`, null, true, 'DELETE');
|
||||
}
|
||||
|
||||
// DOWNLOAD (returns object with card details)
|
||||
async downloadCardBalances(orderId) {
|
||||
return this.makeRequest(`/card-balances-download/${orderId}`, null, true, 'GET');
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Card Requisites Orders (Kart rekwizitler)
|
||||
// ================================
|
||||
|
||||
// LIST
|
||||
async getCardRequisiteOrders() {
|
||||
return this.makeRequest('/card-requisites', null, true, 'GET');
|
||||
}
|
||||
|
||||
// CREATE (passport + card info)
|
||||
async createCardRequisiteOrder(payload) {
|
||||
/* payload expected keys: passport_serie, passport_id, card_number, card_month, card_year, maybe card_name */
|
||||
return this.makeRequest('/card-requisites', payload, true, 'POST');
|
||||
}
|
||||
|
||||
// SHOW
|
||||
async getCardRequisiteOrder(orderId) {
|
||||
return this.makeRequest(`/card-requisites/${orderId}`, null, true, 'GET');
|
||||
}
|
||||
|
||||
// UPDATE
|
||||
async updateCardRequisiteOrder(orderId, payload) {
|
||||
return this.makeRequest(`/card-requisites/${orderId}`, payload, true, 'POST');
|
||||
}
|
||||
|
||||
// DELETE
|
||||
async deleteCardRequisiteOrder(orderId) {
|
||||
return this.makeRequest(`/card-requisites/${orderId}`, null, true, 'DELETE');
|
||||
}
|
||||
|
||||
// DOWNLOAD
|
||||
async downloadCardRequisites(orderId) {
|
||||
return this.makeRequest(`/card-requisites-download/${orderId}`, null, true, 'GET');
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Card Pin Orders (Kart pin bukjalar)
|
||||
// ================================
|
||||
|
||||
// LIST
|
||||
async getCardPinOrders() {
|
||||
return this.makeRequest('/card-pin-order', null, true, 'GET');
|
||||
}
|
||||
|
||||
// CREATE (multipart payload similar to Card Requisite but without month/year)
|
||||
async createCardPinOrder(payload) {
|
||||
return this.makeRequest('/card-pin-order', payload, true, 'POST');
|
||||
}
|
||||
|
||||
// SHOW
|
||||
async getCardPinOrder(orderId) {
|
||||
return this.makeRequest(`/card-pin-order/${orderId}`, null, true, 'GET');
|
||||
}
|
||||
|
||||
// UPDATE
|
||||
async updateCardPinOrder(orderId, payload) {
|
||||
return this.makeRequest(`/card-pin-order/${orderId}`, payload, true, 'POST');
|
||||
}
|
||||
|
||||
// DELETE
|
||||
async deleteCardPinOrder(orderId) {
|
||||
return this.makeRequest(`/card-pin-order/${orderId}`, null, true, 'DELETE');
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Card Orders (Täze kart)
|
||||
// ================================
|
||||
|
||||
// LIST
|
||||
async getCardOrders() {
|
||||
return this.makeRequest('/card-order', null, true, 'GET');
|
||||
}
|
||||
|
||||
// CREATE – returns { message, payment { status, url } }
|
||||
async createCardOrder(payload) {
|
||||
return this.makeRequest('/card-order', payload, true, 'POST');
|
||||
}
|
||||
|
||||
// SHOW
|
||||
async getCardOrder(orderId) {
|
||||
return this.makeRequest(`/card-order/${orderId}`, null, true, 'GET');
|
||||
}
|
||||
|
||||
// DELETE
|
||||
async deleteCardOrder(orderId) {
|
||||
return this.makeRequest(`/card-order/${orderId}`, null, true, 'DELETE');
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Account deletion
|
||||
// ================================
|
||||
|
||||
async deleteUserAccount() {
|
||||
return this.makeRequest('/auth/delete-user', null, true, 'POST');
|
||||
}
|
||||
}
|
||||
|
||||
export default new AuthService();
|
||||