Compare commits

...

10 Commits

Author SHA1 Message Date
89200eacd5 ok 2025-07-09 12:32:05 +05:00
570a4bd335 delete action 2025-07-09 12:03:53 +05:00
4dce2a6d63 card order ready 2025-07-09 11:46:20 +05:00
6044d5ae78 card pins ready 2025-07-09 10:41:15 +05:00
90d9a7b309 card requisite done 2025-07-09 10:14:40 +05:00
7ce0b92f92 bug fixes 2025-07-08 23:58:08 +05:00
89a1c0d9f2 card remaning done 2025-07-08 19:08:49 +05:00
10ac440401 show error message 2025-07-08 17:35:41 +05:00
9ee6dea980 loans done 2025-07-08 17:15:09 +05:00
23ca758917 card balance on homepage 2025-07-08 17:00:17 +05:00
34 changed files with 2957 additions and 105 deletions

View File

@@ -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' },
],
},
```

View File

@@ -20,10 +20,16 @@
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#17b69b"
},
"edgeToEdgeEnabled": true
"edgeToEdgeEnabled": true,
"package": "com.nurmuhammet.ali.tbbankonline"
},
"web": {
"favicon": "./assets/favicon.png"
},
"extra": {
"eas": {
"projectId": "280bed78-9335-4b73-a686-15a9f726a7ad"
}
}
}
}

21
eas.json Normal file
View 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": {}
}
}

16
package-lock.json generated
View File

@@ -23,7 +23,8 @@
"react-native-modal-datetime-picker": "^15.0.1",
"react-native-safe-area-context": "^5.5.1",
"react-native-screens": "^4.11.1",
"react-native-svg": "^15.12.0"
"react-native-svg": "^15.12.0",
"react-native-webview": "^13.11.0"
},
"devDependencies": {
"@babel/core": "^7.20.0"
@@ -6467,6 +6468,19 @@
"react-native": "*"
}
},
"node_modules/react-native-webview": {
"version": "13.15.0",
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz",
"integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==",
"dependencies": {
"escape-string-regexp": "^4.0.0",
"invariant": "2.2.4"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native/node_modules/@react-native/virtualized-lists": {
"version": "0.79.5",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.5.tgz",

View File

@@ -24,7 +24,8 @@
"react-native-safe-area-context": "^5.5.1",
"react-native-screens": "^4.11.1",
"react-native-svg": "^15.12.0",
"expo-image-picker": "~16.1.4"
"expo-image-picker": "~16.1.4",
"react-native-webview": "^13.11.0"
},
"devDependencies": {
"@babel/core": "^7.20.0"

View File

@@ -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": [

View File

@@ -0,0 +1,21 @@
{
"build": {
"preview": {
"android": {
"buildType": "apk"
}
},
"preview2": {
"android": {
"gradleCommand": ":app:assembleRelease"
}
},
"preview3": {
"developmentClient": true
},
"preview4": {
"distribution": "internal"
},
"production": {}
}
}

View File

@@ -268,7 +268,7 @@ const EditProfileModal = ({
</View>
{/* Form */}
<ScrollView style={styles.form} showsVerticalScrollIndicator={false}>
<ScrollView style={styles.form} showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled" contentContainerStyle={{paddingBottom:80}}>
<View style={styles.formSection}>
<Text style={styles.sectionTitle}>Esasy maglumatlar</Text>
@@ -451,7 +451,6 @@ const styles = StyleSheet.create({
width: 32,
},
form: {
flex: 1,
paddingHorizontal: 20,
},
formSection: {

View File

@@ -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>
);

View File

@@ -12,6 +12,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 +41,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>
);

View File

@@ -49,8 +49,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])
);

View File

@@ -0,0 +1,314 @@
import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, SafeAreaView, Modal } from 'react-native';
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;

View File

@@ -0,0 +1,195 @@
import React, { useState, useCallback } from 'react';
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl, SafeAreaView } from 'react-native';
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;

View File

@@ -0,0 +1,133 @@
import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, ScrollView, SafeAreaView, Image, Alert, Linking } from 'react-native';
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;

View File

@@ -0,0 +1,84 @@
import React, { useCallback, useState } from 'react';
import { View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator, RefreshControl, SafeAreaView } from 'react-native';
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 = '#EFF6FF';
const CIRCLE_BG = '#7FB3FF';
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;

View File

@@ -0,0 +1,145 @@
import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, SafeAreaView, Image, Linking } from 'react-native';
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;

View File

@@ -0,0 +1,123 @@
import React, { useState, useCallback } from 'react';
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl, SafeAreaView } from 'react-native';
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;

View File

@@ -0,0 +1,169 @@
import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, SafeAreaView, Image } from 'react-native';
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.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;

View File

@@ -0,0 +1,123 @@
import React, { useState, useCallback, useRef } from 'react';
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl, SafeAreaView } from 'react-native';
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;

View File

@@ -78,7 +78,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');
}
};

View File

@@ -0,0 +1,171 @@
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, SafeAreaView, ScrollView } from 'react-native';
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;

View File

@@ -0,0 +1,201 @@
import React, { useState, useEffect, useRef } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, SafeAreaView, ScrollView, Modal } from 'react-native';
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;

View File

@@ -0,0 +1,149 @@
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, SafeAreaView, ScrollView } from 'react-native';
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;

View File

@@ -0,0 +1,159 @@
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, SafeAreaView, ScrollView } from 'react-native';
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;

View File

@@ -21,6 +21,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 +41,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 +100,11 @@ const CreateCardTransactionOrderScreen = () => {
label="*Kart belgisi"
placeholder="9934..."
value={cardNumber}
onChangeText={setCardNumber}
onChangeText={handleCardNumberChange}
keyboardType="numeric"
autoCapitalize="none"
autoCorrect={false}
maxLength={19}
/>
<SelectInput

View File

@@ -184,13 +184,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 +295,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 +308,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 +350,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 +360,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 +398,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 +414,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} />
</>
)}

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, SafeAreaView } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
@@ -6,19 +6,33 @@ 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 +54,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..."

View File

@@ -59,20 +59,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 +173,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 " 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 +188,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 " 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 +201,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 " 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>
</>
)}

View File

@@ -89,7 +89,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')}>

View File

@@ -19,6 +19,14 @@ const HomeScreen = () => {
const { user, logout } = useAuth();
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 showBalanceCard = !loadingCardBalance && cardBalanceError === null && cardBalance !== null;
// (Optional) Add helpers here if needed in the future
useEffect(() => {
const fetchMetrics = async () => {
@@ -39,6 +47,38 @@ const HomeScreen = () => {
fetchMetrics();
}, []);
useEffect(() => {
const fetchCardBalance = 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);
return;
}
try {
const res = await apiService.getCardBalanceQuickCheck();
if (res.success) {
// Try common balance keys else fallback to raw
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);
} finally {
setLoadingCardBalance(false);
}
};
fetchCardBalance();
}, [user]);
return (
<SafeAreaView style={styles.container}>
<StatusBar style="dark" />
@@ -67,19 +107,25 @@ 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} />
</View>
</View>
)}
</ScrollView>
</SafeAreaView>
);

View File

@@ -34,13 +34,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,8 +50,16 @@ 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);
}

View File

@@ -59,27 +59,27 @@ 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: 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 },
],
},
@@ -103,7 +103,7 @@ const ProfileScreen = () => {
handleSecuritySettings();
break;
default:
Alert.alert('Üns beriň', 'Bu funksiýa entek işlenok');
// Alert.alert('Üns beriň', 'Bu funksiýa entek işlenok');
}
};

View File

@@ -106,9 +106,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 +124,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 +165,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 +368,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();

View File

@@ -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();
@@ -184,15 +192,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 +218,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 +390,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();